diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..3729ff0c
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,25 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
\ No newline at end of file
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 68c1ba37..540c069a 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -37,10 +37,10 @@ jobs:
- name: Docker Compose
run: docker-compose up --build -d
- - name: Sleep for 30 seconds
+ - name: Sleep for 90 seconds
uses: juliangruber/sleep-action@v1
with:
- time: 30s
+ time: 90s
- name: Setup node
uses: actions/setup-node@v1
@@ -48,4 +48,7 @@ jobs:
node-version: '10.15.3'
- name: Postman tests
- run: npm install -g newman && newman run Postman/dex.postman_collection.json -e Postman/local.postman_environment.json --insecure --delay-request=200 -n 3
+ run: npm install -g newman && newman run Postman/dex.postman_collection.json -e Postman/local.postman_environment.json --insecure --reporter-cli-no-console --reporter-cli-no-success-assertions -n 3
+
+ - name: Recommendation tests
+ run: npm install -g newman && newman run Postman/ElasticSearch/DeX_Elastic.postman_collection.json -e Postman/ElasticSearch/DeX_Elastic.postman_environment.json --insecure --reporter-cli-no-console --reporter-cli-no-success-assertions -n 3
diff --git a/API/01_API.csproj b/API/01_API.csproj
index 60fad56c..4c800a78 100644
--- a/API/01_API.csproj
+++ b/API/01_API.csproj
@@ -6,7 +6,7 @@
.\API.xml
Digital Excellence Fontys
8
- 1.1.0-beta
+ 1.2.0-beta
@@ -41,18 +41,17 @@
-
+
-
+
-
diff --git a/API/Configuration/MappingProfile.cs b/API/Configuration/MappingProfile.cs
index 46c544c3..b1d64576 100644
--- a/API/Configuration/MappingProfile.cs
+++ b/API/Configuration/MappingProfile.cs
@@ -20,6 +20,7 @@
using Models;
using Services.ExternalDataProviders;
using Services.ExternalDataProviders.Resources;
+using System.Collections.Generic;
namespace API.Configuration
{
@@ -83,7 +84,8 @@ public MappingProfile()
CreateMap();
- CreateMap();
+ CreateMap()
+ .ForMember(q => q.Categories, opt => opt.Ignore());
CreateMap();
CreateMap();
CreateMap();
@@ -103,6 +105,13 @@ public MappingProfile()
CreateMap();
CreateMap();
+ CreateMap();
+ CreateMap();
+ CreateMap();
+ CreateMap()
+ .ForMember(q => q.Id, opt => opt.MapFrom(q=> q.Category.Id))
+ .ForMember(q => q.Name, opt => opt.MapFrom(q => q.Category.Name));
+
CreateMap();
CreateMap();
@@ -142,20 +151,6 @@ public MappingProfile()
CreateMap();
- CreateExternalSourceMappingProfiles();
- }
-
- private void CreateExternalSourceMappingProfiles()
- {
- CreateMap()
- .ForMember(d => d.Name, opt => opt.MapFrom(m => m.Title));
-
- CreateMap()
- .ForMember(dest => dest.ShortDescription, opt => opt.MapFrom(src => src.Description));
-
- CreateMap()
- .ForMember(dest => dest.ShortDescription, opt => opt.MapFrom(src => src.Description));
-
CreateMap()
.ForMember(dest => dest.OptionValue, opt => opt.MapFrom(src => src.OptionValue.ToLower()));
CreateMap();
@@ -177,6 +172,21 @@ private void CreateExternalSourceMappingProfiles()
.ForMember(dest => dest.InstititutionName, opt => opt.MapFrom(src => src.Institution.Name))
.ForMember(dest => dest.ProjectName, opt => opt.MapFrom(src => src.Project.Name));
+
+ CreateExternalSourceMappingProfiles();
+ }
+
+ private void CreateExternalSourceMappingProfiles()
+ {
+ CreateMap()
+ .ForMember(d => d.Name, opt => opt.MapFrom(m => m.Title));
+
+ CreateMap()
+ .ForMember(dest => dest.ShortDescription, opt => opt.MapFrom(src => src.Description));
+
+ CreateMap()
+ .ForMember(dest => dest.ShortDescription, opt => opt.MapFrom(src => src.Description));
+
}
}
diff --git a/API/Controllers/CategoryController.cs b/API/Controllers/CategoryController.cs
new file mode 100644
index 00000000..fcf3b7e0
--- /dev/null
+++ b/API/Controllers/CategoryController.cs
@@ -0,0 +1,233 @@
+using API.Resources;
+using AutoMapper;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Models;
+using Serilog;
+using Services.Services;
+using System.Collections.Generic;
+using System.Net;
+using System.Threading.Tasks;
+using static Models.Defaults.Defaults;
+
+namespace API.Controllers
+{
+
+ ///
+ /// This class is responsible for handling HTTP requests that are related
+ /// to categories, for example creating, retrieving, updating or deleting.
+ ///
+ [Route("api/[controller]")]
+ [ApiController]
+ public class CategoryController : ControllerBase
+ {
+
+ private readonly IMapper mapper;
+ private readonly ICategoryService categoryService;
+ private readonly IProjectCategoryService projectCategoryService;
+
+ ///
+ /// Initializes a new instance of the class
+ ///
+ /// The category service which is used to communicate with the logic layer.
+ /// The project category service which is used to communicate with the logic layer.
+ /// The mapper which is used to convert the resources to the model to the resource result.
+ public CategoryController(ICategoryService categoryService, IProjectCategoryService projectCategoryService, IMapper mapper)
+ {
+ this.categoryService = categoryService;
+ this.projectCategoryService = projectCategoryService;
+ this.mapper = mapper;
+ }
+
+ ///
+ /// This method is responsible for retrieving all categories.
+ ///
+ /// This method returns a list of category resource results.
+ /// This endpoint returns a list of categories.
+ [HttpGet]
+ [Authorize(Policy = nameof(Scopes.CategoryRead))]
+ [ProducesResponseType(typeof(IEnumerable), (int) HttpStatusCode.OK)]
+ public async Task GetAllCategories()
+ {
+ IEnumerable categories = await categoryService.GetAll()
+ .ConfigureAwait(false);
+
+ return Ok(mapper.Map, IEnumerable>(categories));
+ }
+
+ ///
+ /// This method is responsible for retrieving a single category.
+ ///
+ /// This method returns the category resource result.
+ /// This endpoint returns the category with the specified id.
+ /// The 400 Bad Request status code is returned when the specified category id is invalid.
+ /// The 404 Not Found status code is returned when no category is found with the specified category id.
+ [HttpGet("{categoryId}")]
+ [Authorize(Policy = nameof(Scopes.CategoryRead))]
+ [ProducesResponseType(typeof(CategoryResourceResult), (int) HttpStatusCode.OK)]
+ [ProducesResponseType(typeof(ProblemDetails), (int) HttpStatusCode.BadRequest)]
+ [ProducesResponseType(typeof(ProblemDetails), (int) HttpStatusCode.NotFound)]
+ public async Task GetCategory(int categoryId)
+ {
+ if(categoryId < 0)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed getting category.",
+ Detail =
+ "The Id is smaller then 0 and therefore it could never be a valid category id.",
+ Instance = "758F4B36-A047-42D4-9F9E-B09BF8106F85"
+ };
+ return BadRequest(problem);
+ }
+
+ Category category = await categoryService.FindAsync(categoryId)
+ .ConfigureAwait(false);
+ if(category == null)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed getting category.",
+ Detail = "The category could not be found in the database.",
+ Instance = "872DEE7C-D1C8-4161-B8BA-B577EAA5A1C9"
+ };
+ return NotFound(problem);
+ }
+
+ return Ok(mapper.Map(category));
+ }
+
+ ///
+ /// This method is responsible for creating the category.
+ ///
+ /// The category resource which is used to create a category.
+ /// This method returns the created category resource result.
+ /// This endpoint returns the created category.
+ /// The 400 Bad Request status code is returned when unable to create category.
+ [HttpPost]
+ [Authorize(Policy = nameof(Scopes.CategoryWrite))]
+ [ProducesResponseType(typeof(CategoryResourceResult), (int) HttpStatusCode.Created)]
+ [ProducesResponseType(typeof(ProblemDetails), (int) HttpStatusCode.BadRequest)]
+ public async Task CreateCategoryAsync([FromBody] CategoryResource categoryResource)
+ {
+ if(categoryResource == null)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed to create a new category.",
+ Detail = "The specified category resource was null",
+ Instance = "ABA3B997-1B80-47FC-A72B-69BC0D8DFA93"
+ };
+ return BadRequest(problem);
+ }
+ Category category = mapper.Map(categoryResource);
+
+ try
+ {
+ await categoryService.AddAsync(category)
+ .ConfigureAwait(false);
+ categoryService.Save();
+ return Created(nameof(CreateCategoryAsync), mapper.Map(category));
+ }
+ catch(DbUpdateException e)
+ {
+ Log.Logger.Error(e, "Database exception");
+
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed to save the new category.",
+ Detail = "There was a problem while saving the category to the database.",
+ Instance = "D56DBE55-57A1-4655-99C5-4F4ECEEE3BE4"
+ };
+ return BadRequest(problem);
+ }
+ }
+
+ ///
+ /// This method is responsible for updating the category.
+ ///
+ /// The category identifier which is used for searching the category.
+ /// The category resource which is used to update the category.
+ /// This method returns the updated category resource result.
+ /// This endpoint returns the updated category.
+ /// The 404 Not Found status code is returned when the category with the specified id could not be found.
+ [HttpPut("{categoryId}")]
+ [Authorize(Policy = nameof(Scopes.CategoryWrite))]
+ [ProducesResponseType(typeof(CategoryResourceResult), (int) HttpStatusCode.OK)]
+ [ProducesResponseType(typeof(ProblemDetails), (int) HttpStatusCode.NotFound)]
+ public async Task UpdateCategory(int categoryId, CategoryResource categoryResource)
+ {
+ Category currentCategory = await categoryService.FindAsync(categoryId)
+ .ConfigureAwait(false);
+ if(currentCategory == null)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed to update the category.",
+ Detail = "The specified category could not be found in the database",
+ Instance = "8F167FDF-3B2B-4E71-B3D0-AA2B1C1CE2C3"
+ };
+ return NotFound(problem);
+ }
+ mapper.Map(categoryResource, currentCategory);
+
+ categoryService.Update(currentCategory);
+ categoryService.Save();
+
+ return Ok(mapper.Map(currentCategory));
+ }
+
+ ///
+ /// This method is responsible for deleting the category.
+ ///
+ /// The category identifier which is used for searching the category.
+ /// This method returns status code 200.
+ /// This endpoint returns status code 200. Category is deleted.
+ /// The 404 Not Found status code is returned when the category with the specified id could not be found.
+ /// The 409 Conflict status code is returned when the category is still connected to a project.
+ [HttpDelete("{categoryId}")]
+ [Authorize(Policy = nameof(Scopes.CategoryWrite))]
+ [ProducesResponseType(typeof(CategoryResourceResult), (int) HttpStatusCode.OK)]
+ [ProducesResponseType(typeof(ProblemDetails), (int) HttpStatusCode.Conflict)]
+ [ProducesResponseType(typeof(ProblemDetails), (int) HttpStatusCode.NotFound)]
+ public async Task DeleteCategory(int categoryId)
+ {
+ Category category = await categoryService.FindAsync(categoryId)
+ .ConfigureAwait(false);
+ if(category == null)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed to delete the category.",
+ Detail = "The category could not be found in the database.",
+ Instance = "A0853DE4-C881-4597-A5A7-42F6761CECE0"
+ };
+ return NotFound(problem);
+ }
+
+ ProjectCategory projectCategory = await projectCategoryService.GetProjectCategory(categoryId);
+
+ if(projectCategory != null)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed to delete the category.",
+ Detail = "The category is still connected to a project.",
+ Instance = "4AA5102B-3A6F-4144-BF01-0EC32B4E69A8"
+ };
+ return Conflict(problem);
+ }
+
+ await categoryService.RemoveAsync(category.Id)
+ .ConfigureAwait(false);
+ categoryService.Save();
+
+ IEnumerable categories = await categoryService.GetAll()
+ .ConfigureAwait(false);
+
+ return Ok(mapper.Map, IEnumerable>(categories));
+ }
+ }
+
+}
diff --git a/API/Controllers/DataSourceController.cs b/API/Controllers/DataSourceController.cs
index d88c9b87..d5b5ecbd 100644
--- a/API/Controllers/DataSourceController.cs
+++ b/API/Controllers/DataSourceController.cs
@@ -86,7 +86,6 @@ public DataSourceController(IMapper mapper,
/// This method returns a collection of data sources.
/// This endpoint returns the available data sources with the specified flow.
[HttpGet]
- [Authorize]
[ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
public async Task GetAvailableDataSources([FromQuery] bool? needsAuth)
{
@@ -107,7 +106,7 @@ public async Task GetAvailableDataSources([FromQuery] bool? needs
/// The 404 Not Found status code is returned when no data source with the specified
/// guid could be found.
///
- [HttpGet("guid")]
+ [HttpGet("{guid}")]
[Authorize]
[ProducesResponseType(typeof(DataSourceResourceResult), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
@@ -205,8 +204,9 @@ public async Task UpdateDataSource(string guid, [FromBody] DataSo
return BadRequest(problem);
}
- if(!await wizardPageService.ValidateWizardPagesExist(
- dataSourceResource.WizardPageResources.Select(w => w.WizardPageId)))
+ if(dataSourceResource.WizardPageResources != null &&
+ dataSourceResource.WizardPageResources.Any() && !await wizardPageService.ValidateWizardPagesExist(
+ dataSourceResource.WizardPageResources.Select(w => w.WizardPageId)))
{
ProblemDetails problem = new ProblemDetails
{
diff --git a/API/Controllers/ProjectController.cs b/API/Controllers/ProjectController.cs
index 68bfe4bf..be37a42d 100644
--- a/API/Controllers/ProjectController.cs
+++ b/API/Controllers/ProjectController.cs
@@ -28,6 +28,7 @@
using Models.Defaults;
using Serilog;
using Services.Services;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -53,6 +54,8 @@ public class ProjectController : ControllerBase
private readonly IFileUploader fileUploader;
private readonly IMapper mapper;
private readonly IProjectService projectService;
+ private readonly IProjectCategoryService projectCategoryService;
+ private readonly ICategoryService categoryService;
private readonly IUserProjectLikeService userProjectLikeService;
private readonly IUserProjectService userProjectService;
private readonly IUserService userService;
@@ -79,6 +82,8 @@ public class ProjectController : ControllerBase
/// projects.
///
/// The call to action option service is used to communicate with the logic layer.
+ /// The category service is used to work with categories
+ /// The project category service is used to connect projects and categories
/// The projectinstitution service is responsible for link projects and institutions.
/// The institution service which is used to communicate with the logic layer
public ProjectController(IProjectService projectService,
@@ -89,9 +94,11 @@ public ProjectController(IProjectService projectService,
IAuthorizationHelper authorizationHelper,
IFileUploader fileUploader,
IUserProjectService userProjectService,
- ICallToActionOptionService callToActionOptionService,
IProjectInstitutionService projectInstitutionService,
- IInstitutionService institutionService)
+ IInstitutionService institutionService,
+ ICallToActionOptionService callToActionOptionService,
+ ICategoryService categoryService,
+ IProjectCategoryService projectCategoryService)
{
this.projectService = projectService;
this.userService = userService;
@@ -102,10 +109,39 @@ public ProjectController(IProjectService projectService,
this.authorizationHelper = authorizationHelper;
this.userProjectService = userProjectService;
this.callToActionOptionService = callToActionOptionService;
+ this.categoryService = categoryService;
+ this.projectCategoryService = projectCategoryService;
this.projectInstitutionService = projectInstitutionService;
this.institutionService = institutionService;
}
+
+
+ ///
+ /// This method is responsible for retrieving all projects in ElasticSearch formatted model. After these project are retrieved the endpoint registers the projects at the messagebroker to synchronize.
+ ///
+ /// This method returns a list of in ElasticSearch formatted projects.
+ /// This endpoint returns a list of in ElasticSearch formatted projects.
+ [HttpGet("export")]
+ [Authorize(Policy = nameof(Defaults.Scopes.AdminProjectExport))]
+ [ProducesResponseType(typeof(ProjectResultsResource), (int) HttpStatusCode.OK)]
+ [ProducesResponseType(typeof(ProblemDetails), (int) HttpStatusCode.BadRequest)]
+ public async Task MigrateDatabaseToElasticSearch()
+ {
+ try
+ {
+ List projectsToExport = await projectService.GetAllWithUsersCollaboratorsAndInstitutionsAsync();
+ projectService.MigrateDatabase(projectsToExport);
+
+ return Ok(mapper.Map, List>(projectsToExport));
+ } catch(Exception e)
+ {
+ Log.Logger.Error(e.Message);
+ return BadRequest(e.Message);
+ }
+
+ }
+
///
/// This method is responsible for retrieving all projects.
///
@@ -116,6 +152,8 @@ public ProjectController(IProjectService projectService,
/// The 400 Bad Request status code is returned when values inside project filter params resource are
/// invalid.
///
+ ///
+
[HttpGet]
[ProducesResponseType(typeof(ProjectResultsResource), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(ProblemDetails), (int) HttpStatusCode.BadRequest)]
@@ -329,6 +367,35 @@ await callToActionOptionService.GetCallToActionOptionFromValueAsync(
project.User = await HttpContext.GetContextUser(userService)
.ConfigureAwait(false);
+ if(projectResource.Categories != null)
+ {
+ ICollection projectCategoryResources = projectResource.Categories;
+
+ foreach(ProjectCategoryResource projectCategoryResource in projectCategoryResources)
+ {
+ ProjectCategory alreadyExcProjectCategory = await projectCategoryService.GetProjectCategory(project.Id, projectCategoryResource.CategoryId);
+ if(alreadyExcProjectCategory == null)
+ {
+ Category category = await categoryService.FindAsync(projectCategoryResource.CategoryId);
+
+ if(category == null)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed to save new project.",
+ Detail = "One of the given categories did not exist.",
+ Instance = "C152D170-F9C2-48DE-8111-02DBD160C768"
+ };
+ return BadRequest(problem);
+ }
+
+ ProjectCategory projectCategory = new ProjectCategory(project, category);
+ await projectCategoryService.AddAsync(projectCategory)
+ .ConfigureAwait(false);
+ }
+ }
+ }
+
if(project.InstitutePrivate)
{
if(project.User.InstitutionId == default)
@@ -349,6 +416,9 @@ await callToActionOptionService.GetCallToActionOptionFromValueAsync(
{
projectService.Add(project);
projectService.Save();
+
+ projectCategoryService.Save();
+
return Created(nameof(CreateProjectAsync), mapper.Map(project));
} catch(DbUpdateException e)
{
@@ -480,6 +550,36 @@ await callToActionOptionService.GetCallToActionOptionFromValueAsync(
}
}
+ await projectCategoryService.ClearProjectCategories(project);
+ if(projectResource.Categories != null)
+ {
+ ICollection projectCategoryResources = projectResource.Categories;
+
+ foreach(ProjectCategoryResource projectCategoryResource in projectCategoryResources)
+ {
+ ProjectCategory alreadyExcProjectCategory = await projectCategoryService.GetProjectCategory(project.Id, projectCategoryResource.CategoryId);
+ if(alreadyExcProjectCategory == null)
+ {
+ Category category = await categoryService.FindAsync(projectCategoryResource.CategoryId);
+
+ if(category == null)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed to update project.",
+ Detail = "One of the given categories did not exist.",
+ Instance = "09D1458E-B2CF-4F23-B120-DDD38A7727C9"
+ };
+ return BadRequest(problem);
+ }
+
+ ProjectCategory projectCategory = new ProjectCategory(project, category);
+ await projectCategoryService.AddAsync(projectCategory)
+ .ConfigureAwait(false);
+ }
+ }
+ }
+
mapper.Map(projectResource, project);
projectService.Update(project);
projectService.Save();
@@ -562,6 +662,8 @@ await fileService.RemoveAsync(fileToDelete.Id)
}
}
+ await projectCategoryService.ClearProjectCategories(project);
+
await projectService.RemoveAsync(projectId)
.ConfigureAwait(false);
projectService.Save();
@@ -573,7 +675,10 @@ await projectService.RemoveAsync(projectId)
/// Follows a project with given projectId and gets userId
///
///
- /// 200 if success 409 if user already follows project
+ /// Project was followed.
+ /// Project or user could not be found.
+ /// User already follows this project.
+ ///
[HttpPost("follow/{projectId}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
@@ -629,6 +734,9 @@ public async Task FollowProject(int projectId)
/// Unfollows project
///
///
+ /// Project was unfollowed.
+ /// Project or user could not be found.
+ /// User does not follow this project.
///
[HttpDelete("follow/{projectId}")]
[Authorize]
@@ -685,11 +793,11 @@ public async Task UnfollowProject(int projectId)
/// Likes an individual project with the provided projectId.
///
/// The project identifier.
- ///
- /// StatusCode 200 If success,
- /// StatusCode 409 If the user already liked the project,
- /// StatusCode 404 if the project could not be found.
- ///
+ /// Project was liked.
+ /// Database failed to like the project.
+ /// Project could not be found.
+ /// User already liked this project before.
+ ///
[HttpPost("like/{projectId}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
@@ -743,8 +851,10 @@ public async Task LikeProject(int projectId)
ProjectLike like = new ProjectLike(projectToLike, currentUser);
await userProjectLikeService.AddAsync(like)
.ConfigureAwait(false);
-
userProjectLikeService.Save();
+
+ // Update Elastic Database about this change.
+ await userProjectLikeService.SyncProjectToES(projectToLike);
return Ok(mapper.Map(like));
} catch(DbUpdateException e)
{
@@ -764,11 +874,10 @@ await userProjectLikeService.AddAsync(like)
/// Unlikes an individual project with the provided projectId.
///
/// The project identifier.
- ///
- /// StatusCode 200 If success,
- /// StatusCode 409 if the user didn't like the project already,
- /// StatusCode 404 if the project could not be found.
- ///
+ /// Project was unliked.
+ /// Project could not be found.
+ /// User did not like this project before.
+ ///
[HttpDelete("like/{projectId}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
@@ -821,6 +930,9 @@ public async Task UnlikeProject(int projectId)
userProjectLikeService.Remove(projectLike);
userProjectLikeService.Save();
+
+ // Update Elastic Database about this change.
+ await userProjectLikeService.SyncProjectToES(projectToLike);
return Ok(mapper.Map(projectLike));
}
@@ -946,7 +1058,6 @@ public async Task LinkInstitutionToProjectAsync(int projectId, in
};
return NotFound(problemDetails);
}
-
if(!await projectService.ProjectExistsAsync(projectId))
{
ProblemDetails problemDetails = new ProblemDetails
@@ -1058,6 +1169,176 @@ public async Task UnlinkInstitutionFromProjectAsync(int projectId
return Ok();
}
+
+ ///
+ /// Categorize a project
+ ///
+ ///
+ ///
+ /// Category was added to the project.
+ /// User is not authorized to add the category to the project.
+ /// Project or category was not found.
+ /// Project is already categorized with the category.
+ ///
+ [HttpPost("category/{projectId}/{categoryId}")]
+ [Authorize]
+ [ProducesResponseType(typeof(ProjectResourceResult), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
+ public async Task ProjectAddCategory(int projectId, int categoryId)
+ {
+ Project project = await projectService.FindAsync(projectId)
+ .ConfigureAwait(false);
+ if(project == null)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed to categorize the project.",
+ Detail = "The project could not be found in the database.",
+ Instance = "1C8D069D-E6CE-43E2-9CF9-D82C0A71A292"
+ };
+ return NotFound(problem);
+ }
+
+ Category category = await categoryService.FindAsync(categoryId)
+ .ConfigureAwait(false);
+
+ if(category == null)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed to categorize the project.",
+ Detail = "The category could not be found in the database.",
+ Instance = "93C6B5BD-EC14-482A-9907-001C888F3D3F"
+ };
+ return NotFound(problem);
+ }
+
+ User user = await HttpContext.GetContextUser(userService)
+ .ConfigureAwait(false);
+ bool isAllowed = await authorizationHelper.UserIsAllowed(user,
+ nameof(Defaults.Scopes.AdminProjectWrite),
+ nameof(Defaults.Scopes.InstitutionProjectWrite),
+ project.UserId);
+
+ if(project.UserId != user.Id && !isAllowed)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed to categorize the project.",
+ Detail = "The user is not allowed to modify the project.",
+ Instance = "1243016C-081F-441C-A388-3D56B0998D2E"
+ };
+ return Unauthorized(problem);
+ }
+
+ ProjectCategory alreadyCategorized = await projectCategoryService.GetProjectCategory(projectId, categoryId);
+
+ if(alreadyCategorized != null)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed to categorize the project.",
+ Detail = "Project has already been categorized with this category.",
+ Instance = "4986CBC6-FB6D-4255-ACE8-833E92B25FBD"
+ };
+ return Conflict(problem);
+ }
+
+
+ ProjectCategory projectCategory = new ProjectCategory(project, category);
+ await projectCategoryService.AddAsync(projectCategory)
+ .ConfigureAwait(false);
+
+ projectCategoryService.Save();
+
+ return Ok(mapper.Map(project));
+ }
+
+ ///
+ /// Remove a category from a project
+ ///
+ ///
+ ///
+ /// Category was removed from the project.
+ /// User is not authorized to remove the category from the project.
+ /// Project or category was not found.
+ /// Project is not categorized with the category.
+ ///
+ [HttpDelete("category/{projectId}/{categoryId}")]
+ [Authorize]
+ [ProducesResponseType(typeof(ProjectResourceResult), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
+ public async Task ProjectRemoveCategory(int projectId, int categoryId)
+ {
+ Project project = await projectService.FindAsync(projectId)
+ .ConfigureAwait(false);
+ if(project == null)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed to remove the category from the project.",
+ Detail = "The project could not be found in the database.",
+ Instance = "2CC94251-9103-4AAC-B461-F99939E78AD0"
+ };
+ return NotFound(problem);
+ }
+
+ Category category = await categoryService.FindAsync(categoryId)
+ .ConfigureAwait(false);
+
+ if(category == null)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed to remove the category from the project.",
+ Detail = "The category could not be found in the database.",
+ Instance = "3E41B5DC-F78B-429B-89AB-1A98A6F65FDC"
+ };
+ return NotFound(problem);
+ }
+
+ User user = await HttpContext.GetContextUser(userService)
+ .ConfigureAwait(false);
+ bool isAllowed = await authorizationHelper.UserIsAllowed(user,
+ nameof(Defaults.Scopes.AdminProjectWrite),
+ nameof(Defaults.Scopes.InstitutionProjectWrite),
+ project.UserId);
+
+ if(project.UserId != user.Id && !isAllowed)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed to remove the category from the project.",
+ Detail = "The user is not allowed to modify the project.",
+ Instance = "4D1878C1-1606-4224-841A-73F30AE4F930"
+ };
+ return Unauthorized(problem);
+ }
+
+ ProjectCategory alreadyCategorized = await projectCategoryService.GetProjectCategory(projectId, categoryId);
+
+ if(alreadyCategorized == null)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed to remove the category from the project.",
+ Detail = "Project has not been categorized with this category.",
+ Instance = "5EABA4F3-47E6-45A7-8522-E87268716912"
+ };
+ return Conflict(problem);
+ }
+
+ await projectCategoryService.RemoveAsync(alreadyCategorized.Id)
+ .ConfigureAwait(false);
+
+ projectCategoryService.Save();
+
+ return Ok(mapper.Map(project));
+ }
}
}
diff --git a/API/Controllers/UserController.cs b/API/Controllers/UserController.cs
index 21558071..a78f5485 100644
--- a/API/Controllers/UserController.cs
+++ b/API/Controllers/UserController.cs
@@ -16,15 +16,18 @@
*/
using API.Common;
+using API.Configuration;
using API.Extensions;
using API.Resources;
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
+using Microsoft.VisualStudio.Web.CodeGeneration;
using Models;
using Models.Defaults;
using Serilog;
+using Services.Resources;
using Services.Services;
using System.Collections.Generic;
using System.Linq;
@@ -594,6 +597,46 @@ public async Task SetUserGraduationDate([FromBody] UserResource u
return Ok(mapper.Map(user));
}
+
+ ///
+ /// Get recommended projects for the user who is logged in.
+ ///
+ /// Ok
+ [HttpGet("projectrecommendations/{amount}")]
+ [ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.NotFound)]
+ [Authorize]
+ public async Task GetRecommendedProjects(int amount)
+ {
+ User user = await HttpContext.GetContextUser(userService).ConfigureAwait(false);
+
+ if(await userService.FindAsync(user.Id) == null)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed getting the user account.",
+ Detail = "The database does not contain a user with this user id.",
+ Instance = "1245BE3A-A200-4275-8622-D2D8ECEC55D3"
+ };
+ return NotFound(problem);
+ }
+
+ try
+ {
+ List projectRecommendations = await userService.GetRecommendedProjects(user.Id, amount);
+ return Ok(mapper.Map, List>(projectRecommendations));
+
+ } catch(RecommendationNotFoundException e)
+ {
+ ProblemDetails problem = new ProblemDetails
+ {
+ Title = "Failed getting the recommendations",
+ Detail = e.Message,
+ Instance = "948319D2-1A19-4E00-AF50-DB5D096AFD39"
+ };
+ return NotFound(problem);
+ }
+
+ }
}
}
diff --git a/API/Controllers/WizardController.cs b/API/Controllers/WizardController.cs
index 1d2991b2..1cc49afa 100644
--- a/API/Controllers/WizardController.cs
+++ b/API/Controllers/WizardController.cs
@@ -266,7 +266,7 @@ public async Task GetProjectsFromExternalDataSource(
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task GetProjectByGuidFromExternalDataSource([FromQuery] string dataSourceGuid,
[FromQuery] string token,
- int projectId,
+ string projectId,
[FromQuery] bool needsAuth)
{
if(!Guid.TryParse(dataSourceGuid, out Guid _))
diff --git a/API/Controllers/WizardPageController.cs b/API/Controllers/WizardPageController.cs
index 67588207..427a3fac 100644
--- a/API/Controllers/WizardPageController.cs
+++ b/API/Controllers/WizardPageController.cs
@@ -22,6 +22,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Models;
+using Models.Defaults;
using Serilog;
using Services.Services;
using System.Collections.Generic;
@@ -127,7 +128,7 @@ public async Task GetPageById(int id)
/// when a database update exception occured.
///
[HttpPost]
- [Authorize]
+ [Authorize(Policy = nameof(Defaults.Scopes.WizardPageWrite))]
[ProducesResponseType(typeof(WizardPageResourceResult), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task CreateWizardPage(WizardPageResource wizardPageResource)
@@ -178,7 +179,7 @@ public async Task CreateWizardPage(WizardPageResource wizardPageR
/// the specified id.
///
[HttpPut("{id}")]
- [Authorize]
+ [Authorize(Policy = nameof(Defaults.Scopes.WizardPageWrite))]
[ProducesResponseType(typeof(WizardPageResourceResult), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
@@ -229,7 +230,7 @@ public async Task UpdatedWizardPage(int id, [FromBody] WizardPage
/// the specified id.
///
[HttpDelete("{id}")]
- [Authorize]
+ [Authorize(Policy = nameof(Defaults.Scopes.WizardPageWrite))]
[ProducesResponseType(typeof(WizardPageResourceResult), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
diff --git a/API/Extensions/DependencyInjectionExtensions.cs b/API/Extensions/DependencyInjectionExtensions.cs
index 9fcb3a9e..7b73fd1d 100644
--- a/API/Extensions/DependencyInjectionExtensions.cs
+++ b/API/Extensions/DependencyInjectionExtensions.cs
@@ -24,6 +24,9 @@
using Microsoft.Extensions.DependencyInjection;
using Repositories;
using Services.ExternalDataProviders;
+using Services.ExternalDataProviders.Helpers;
+using Repositories.ElasticSearch;
+using Services.Resources;
using Services.Services;
using Services.Sources;
@@ -62,6 +65,12 @@ public static IServiceCollection AddServicesAndRepositories(this IServiceCollect
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
+ services.AddScoped();
+ services.AddScoped();
+
services.AddScoped();
services.AddScoped();
@@ -100,29 +109,31 @@ public static IServiceCollection AddServicesAndRepositories(this IServiceCollect
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
+ services.AddScoped();
+ services.AddScoped();
+
+ services.AddSingleton();
+ services.AddSingleton();
+
+ services.AddSingleton();
+
services.AddScoped();
services.AddScoped();
-
+
services.AddExternalDataSources();
return services;
+
}
- private static IServiceCollection AddExternalDataSources(this IServiceCollection services)
+ private static void AddExternalDataSources(this IServiceCollection services)
{
services.AddScoped();
services.AddScoped();
services.AddScoped();
-
- services.AddScoped();
- services.AddScoped();
-
- services.AddScoped();
- services.AddScoped();
-
- services.AddScoped();
-
- return services;
}
}
diff --git a/API/HelperClasses/EmailSender.cs b/API/HelperClasses/EmailSender.cs
new file mode 100644
index 00000000..016716f7
--- /dev/null
+++ b/API/HelperClasses/EmailSender.cs
@@ -0,0 +1,48 @@
+using MessageBrokerPublisher;
+using MessageBrokerPublisher.Models;
+using MessageBrokerPublisher.Services;
+
+namespace API.HelperClasses
+{
+ ///
+ /// EmailSender Interface
+ ///
+ public interface IEmailSender
+ {
+ ///
+ /// Method to send email
+ ///
+ /// The email address of the recipient
+ /// The text content of the email
+ /// The HTML content of the email
+ public void Send(string recipient, string textContent, string htmlContent);
+ }
+
+ ///
+ /// Class which is responsible for sending emails
+ ///
+ public class EmailSender : IEmailSender
+ {
+ private readonly ITaskPublisher taskPublisher;
+
+ ///
+ /// Constructor to instantiate the email sender
+ ///
+ public EmailSender(ITaskPublisher taskPublisher)
+ {
+ this.taskPublisher = taskPublisher;
+ }
+
+ ///
+ /// Method to send email
+ ///
+ /// The email address of the recipient
+ /// The text content of the email
+ /// The HTML content of the email
+ public void Send(string recipient, string textContent, string htmlContent = null)
+ {
+ EmailNotificationRegister emailNotification = new EmailNotificationRegister(recipient, textContent, htmlContent);
+ taskPublisher.RegisterTask(Newtonsoft.Json.JsonConvert.SerializeObject(emailNotification), Subject.EMAIL);
+ }
+ }
+}
diff --git a/API/HelperClasses/SeedHelper.cs b/API/HelperClasses/SeedHelper.cs
index 282dee40..b42f6b33 100644
--- a/API/HelperClasses/SeedHelper.cs
+++ b/API/HelperClasses/SeedHelper.cs
@@ -82,6 +82,69 @@ public static void InsertUser(User seedUser, ApplicationDbContext context)
context.SaveChanges();
}
+ ///
+ /// This method checks if the data sources has a collection of wizard pages. If not,
+ /// they will get added to the data source. This is just for testing purposes and does
+ /// not have to get extended when new data sources are added.
+ ///
+ ///
+ public static void SeedDataSourceWizardPages(ApplicationDbContext context)
+ {
+ foreach(DataSource dataSource in context.DataSource.Include(wp => wp.DataSourceWizardPages))
+ {
+ if(dataSource.DataSourceWizardPages == null || !dataSource.DataSourceWizardPages.Any())
+ {
+ dataSource.DataSourceWizardPages = new List();
+ // Github
+ if(dataSource.Guid == "de38e528-1d6d-40e7-83b9-4334c51c19be")
+ {
+ if(context.WizardPage.FirstOrDefault(p => p.Id == 2) != null)
+ dataSource.DataSourceWizardPages.Add(new DataSourceWizardPage
+ {
+ AuthFlow = false,
+ DataSourceId = dataSource.Id,
+ OrderIndex = 1,
+ WizardPageId = 2
+ });
+ }
+ // Gitlab
+ else if(dataSource.Guid == "66de59d4-5db0-4bf8-a9a5-06abe8d3443a")
+ {
+ if(context.WizardPage.FirstOrDefault(p => p.Id == 2) != null)
+ dataSource.DataSourceWizardPages.Add(new DataSourceWizardPage
+ {
+ AuthFlow = false,
+ DataSourceId = dataSource.Id,
+ OrderIndex = 1,
+ WizardPageId = 2
+ });
+ }
+ // JsFiddle
+ else if(dataSource.Guid == "96666870-3afe-44e2-8d62-337d49cf972d")
+ {
+ if(context.WizardPage.FirstOrDefault(p => p.Id == 1) != null)
+ dataSource.DataSourceWizardPages.Add(new DataSourceWizardPage
+ {
+ AuthFlow = false,
+ DataSourceId = dataSource.Id,
+ OrderIndex = 1,
+ WizardPageId = 1
+ });
+
+ if(context.WizardPage.FirstOrDefault(p => p.Id == 3) != null)
+ dataSource.DataSourceWizardPages.Add(new DataSourceWizardPage
+ {
+ AuthFlow = false,
+ DataSourceId = dataSource.Id,
+ OrderIndex = 2,
+ WizardPageId = 3
+ });
+ }
+ }
+ }
+ context.SaveChanges();
+ }
+
}
}
diff --git a/API/Properties/launchSettings.json b/API/Properties/launchSettings.json
index 59c318b0..d0b0fc26 100644
--- a/API/Properties/launchSettings.json
+++ b/API/Properties/launchSettings.json
@@ -1,14 +1,13 @@
{
- "$schema": "http://json.schemastore.org/launchsettings.json",
- "profiles": {
- "Dex-API": {
- "commandName": "Project",
- "launchBrowser": true,
- "launchUrl": "",
- "applicationUrl": "https://localhost:5001",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
- }
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "Dex-API": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:5001"
}
+ }
}
diff --git a/API/Resources/CategoryResource.cs b/API/Resources/CategoryResource.cs
new file mode 100644
index 00000000..3ca954ad
--- /dev/null
+++ b/API/Resources/CategoryResource.cs
@@ -0,0 +1,41 @@
+/*
+* Digital Excellence Copyright (C) 2020 Brend Smits
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Lesser General Public License as published
+* by the Free Software Foundation version 3 of the License.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty
+* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+* See the GNU Lesser General Public License for more details.
+*
+* You can find a copy of the GNU Lesser General Public License
+* along with this program, in the LICENSE.md file in the root project directory.
+* If not, see https://www.gnu.org/licenses/lgpl-3.0.txt
+*/
+
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace API.Resources
+{
+
+ ///
+ /// The view model of a category
+ ///
+ public class CategoryResource
+ {
+
+ ///
+ /// Gets or sets the name.
+ ///
+ ///
+ /// The name.
+ ///
+ [Required]
+ public string Name { get; set; }
+
+ }
+
+}
diff --git a/API/Resources/CategoryResourceResult.cs b/API/Resources/CategoryResourceResult.cs
new file mode 100644
index 00000000..c413a40e
--- /dev/null
+++ b/API/Resources/CategoryResourceResult.cs
@@ -0,0 +1,41 @@
+/*
+* Digital Excellence Copyright (C) 2020 Brend Smits
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Lesser General Public License as published
+* by the Free Software Foundation version 3 of the License.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty
+* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+* See the GNU Lesser General Public License for more details.
+*
+* You can find a copy of the GNU Lesser General Public License
+* along with this program, in the LICENSE.md file in the root project directory.
+* If not, see https://www.gnu.org/licenses/lgpl-3.0.txt
+*/
+
+using System.Collections.Generic;
+
+namespace API.Resources
+{
+
+ ///
+ /// The view model of a Category
+ ///
+ public class CategoryResourceResult
+ {
+
+ ///
+ /// Get or Set the Id of a Category
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Get or Set the Name of a Category
+ ///
+ public string Name { get; set; }
+
+ }
+
+}
diff --git a/API/Resources/ProjectCategoryResource.cs b/API/Resources/ProjectCategoryResource.cs
new file mode 100644
index 00000000..9068a491
--- /dev/null
+++ b/API/Resources/ProjectCategoryResource.cs
@@ -0,0 +1,33 @@
+/*
+* Digital Excellence Copyright (C) 2020 Brend Smits
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Lesser General Public License as published
+* by the Free Software Foundation version 3 of the License.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty
+* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+* See the GNU Lesser General Public License for more details.
+*
+* You can find a copy of the GNU Lesser General Public License
+* along with this program, in the LICENSE.md file in the root project directory.
+* If not, see https://www.gnu.org/licenses/lgpl-3.0.txt
+*/
+
+namespace API.Resources
+{
+
+ ///
+ /// Object to retrieve from tthe frontend with the ProjectCategory
+ ///
+ public class ProjectCategoryResource
+ {
+
+ ///
+ /// Gets or sets the Id of the category.
+ ///
+ public int CategoryId { get; set; }
+ }
+
+}
diff --git a/API/Resources/ProjectCategoryResourceResult.cs b/API/Resources/ProjectCategoryResourceResult.cs
new file mode 100644
index 00000000..c75e57c3
--- /dev/null
+++ b/API/Resources/ProjectCategoryResourceResult.cs
@@ -0,0 +1,39 @@
+/*
+* Digital Excellence Copyright (C) 2020 Brend Smits
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Lesser General Public License as published
+* by the Free Software Foundation version 3 of the License.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty
+* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+* See the GNU Lesser General Public License for more details.
+*
+* You can find a copy of the GNU Lesser General Public License
+* along with this program, in the LICENSE.md file in the root project directory.
+* If not, see https://www.gnu.org/licenses/lgpl-3.0.txt
+*/
+
+namespace API.Resources
+{
+
+ ///
+ /// Object to return to frontend with the ProjectCategory
+ ///
+ public class ProjectCategoryResourceResult
+ {
+
+ ///
+ /// Gets or sets the Id of the project category.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Gets or sets the Name of the project category.
+ ///
+ public string Name { get; set; }
+
+ }
+
+}
diff --git a/API/Resources/ProjectResource.cs b/API/Resources/ProjectResource.cs
index 3af6d3ff..e7eabb91 100644
--- a/API/Resources/ProjectResource.cs
+++ b/API/Resources/ProjectResource.cs
@@ -66,6 +66,11 @@ public class ProjectResource
///
public bool InstitutePrivate { get; set; }
+ ///
+ /// This gets or sets the categories
+ ///
+ public ICollection Categories { get; set; }
+
}
}
diff --git a/API/Resources/ProjectResourceResult.cs b/API/Resources/ProjectResourceResult.cs
index 4d8768fd..3955d8e2 100644
--- a/API/Resources/ProjectResourceResult.cs
+++ b/API/Resources/ProjectResourceResult.cs
@@ -102,6 +102,11 @@ public class ProjectResourceResult
///
public bool InstitutePrivate { get; set; }
+ ///
+ /// This gets or sets the categories belonging to a project.
+ ///
+ public List Categories { get; set; }
+
}
}
diff --git a/API/Resources/ProjectResultResource.cs b/API/Resources/ProjectResultResource.cs
index b91b6212..91e7bade 100644
--- a/API/Resources/ProjectResultResource.cs
+++ b/API/Resources/ProjectResultResource.cs
@@ -77,6 +77,11 @@ public class ProjectResultResource
///
public List Likes { get; set; }
+ ///
+ /// This gets or sets the categories of the project.
+ ///
+ public List Categories { get; set; }
+
}
}
diff --git a/API/Startup.cs b/API/Startup.cs
index d43843cc..2bc5f0f7 100644
--- a/API/Startup.cs
+++ b/API/Startup.cs
@@ -41,6 +41,7 @@
using Newtonsoft.Json;
using Polly;
using Serilog;
+using Services.Resources;
using Services.Services;
using Swashbuckle.AspNetCore.SwaggerUI;
using System;
@@ -68,6 +69,9 @@ public Startup(IConfiguration configuration, IWebHostEnvironment environment)
{
Config = configuration.GetSection("App")
.Get();
+ ElasticConfig = configuration.GetSection("App")
+ .GetSection("Elastic")
+ .Get();
Config.OriginalConfiguration = configuration;
Environment = environment;
}
@@ -77,6 +81,11 @@ public Startup(IConfiguration configuration, IWebHostEnvironment environment)
///
public Config Config { get; }
+ ///
+ /// Config file of Elastic
+ ///
+ public ElasticConfig ElasticConfig { get; }
+
///
/// Environment of the API
///
@@ -94,10 +103,11 @@ public void ConfigureServices(IServiceCollection services)
o.UseSqlServer(Config.OriginalConfiguration.GetConnectionString("DefaultConnection"),
sqlOptions => sqlOptions.EnableRetryOnFailure(50, TimeSpan.FromSeconds(30), null));
});
- services.AddScoped(
- c => new RabbitMQConnectionFactory(Config.RabbitMQ.Hostname,
- Config.RabbitMQ.Username,
- Config.RabbitMQ.Password));
+
+ services.AddSingleton(c => new RabbitMQConnectionFactory(Config.RabbitMQ.Hostname, Config.RabbitMQ.Username, Config.RabbitMQ.Password));
+
+ services.AddSingleton(new ElasticSearchContext(ElasticConfig.Hostname, ElasticConfig.Username, ElasticConfig.Password, ElasticConfig.IndexUrl));
+
services.AddAutoMapper();
services.UseConfigurationValidation();
@@ -148,6 +158,11 @@ public void ConfigureServices(IServiceCollection services)
o.AddPolicy(nameof(Defaults.Scopes.RoleWrite),
policy => policy.Requirements.Add(new ScopeRequirement(nameof(Defaults.Scopes.RoleWrite))));
+ o.AddPolicy(nameof(Defaults.Scopes.CategoryRead),
+ policy => policy.Requirements.Add(new ScopeRequirement(nameof(Defaults.Scopes.CategoryRead))));
+ o.AddPolicy(nameof(Defaults.Scopes.CategoryWrite),
+ policy => policy.Requirements.Add(new ScopeRequirement(nameof(Defaults.Scopes.CategoryWrite))));
+
o.AddPolicy(nameof(Defaults.Scopes.EmbedRead),
policy => policy.Requirements.Add(new ScopeRequirement(nameof(Defaults.Scopes.EmbedRead))));
o.AddPolicy(nameof(Defaults.Scopes.EmbedWrite),
@@ -188,6 +203,14 @@ public void ConfigureServices(IServiceCollection services)
o.AddPolicy(nameof(Defaults.Scopes.UserTaskWrite),
policy => policy.Requirements.Add(
new ScopeRequirement(nameof(Defaults.Scopes.UserTaskWrite))));
+
+ o.AddPolicy(nameof(Defaults.Scopes.WizardPageWrite),
+ policy => policy.Requirements.Add(
+ new ScopeRequirement(nameof(Defaults.Scopes.WizardPageWrite))));
+
+ o.AddPolicy(nameof(Defaults.Scopes.AdminProjectExport),
+ policy => policy.Requirements.Add(new ScopeRequirement(nameof(Defaults.Scopes.AdminProjectExport))));
+
});
services.AddCors();
@@ -271,6 +294,7 @@ public void ConfigureServices(IServiceCollection services)
// Add application services.
services.AddSingleton(Config);
+ services.AddSingleton(ElasticConfig);
services.AddServicesAndRepositories();
services.AddProblemDetails();
}
@@ -449,6 +473,12 @@ private static void UpdateDatabase(IApplicationBuilder app, IWebHostEnvironment
if(!env.IsProduction())
{
+ if(!context.Category.Any())
+ {
+ context.Category.AddRange(Seed.SeedCategories());
+ context.SaveChanges();
+ }
+
if(!context.Institution.Any())
{
// Seed institutions
@@ -489,11 +519,20 @@ private static void UpdateDatabase(IApplicationBuilder app, IWebHostEnvironment
context.Highlight.AddRange(Seed.SeedHighlights(projects));
context.SaveChanges();
}
-
-
+
// TODO seed embedded projects
}
+ if(context.User.FirstOrDefault(e => e.IdentityId == "74489498") != null)
+ {
+ context.User.Remove(context.User.FirstOrDefault(e => e.IdentityId == "74489498"));
+ context.SaveChanges();
+ }
+
+ context.User.Add(Seed.SeedAdminUser2(roles));
+ context.SaveChanges();
+
+
// Seed call to action options
List options = Seed.SeedCallToActionOptions();
foreach(CallToActionOption callToActionOption in options)
@@ -505,6 +544,19 @@ private static void UpdateDatabase(IApplicationBuilder app, IWebHostEnvironment
context.SaveChanges();
}
}
+
+ if(!context.WizardPage.Any())
+ {
+ context.WizardPage.AddRange(Seed.SeedWizardPages());
+ context.SaveChanges();
+ }
+ if(!context.DataSource.Any())
+ {
+ context.DataSource.AddRange(Seed.SeedDataSources());
+ context.SaveChanges();
+ }
+
+ SeedHelper.SeedDataSourceWizardPages(context);
}
///
diff --git a/API/appsettingsapi.Development.json b/API/appsettingsapi.Development.json
index 2917cae3..83aaf5ed 100644
--- a/API/appsettingsapi.Development.json
+++ b/API/appsettingsapi.Development.json
@@ -1,7 +1,6 @@
{
"ConnectionStrings": {
- "DefaultConnection":
- "Server=(LocalDb)\\MSSQLLocalDB;Database=Dex;Trusted_Connection=True;MultipleActiveResultSets=true"
+ "DefaultConnection": "Server=(LocalDb)\\MSSQLLocalDB;Database=Dex;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"App": {
"Frontend": {
@@ -15,6 +14,12 @@
"ClientId": "dex-api",
"ClientSecret": "kP+O2lU!u><.5Avx@MMe4b||_^l"
},
+ "Elastic": {
+ "Hostname": "localhost",
+ "Username": "elastic",
+ "Password": "changeme",
+ "IndexUrl": "projects/"
+ },
"Swagger": {
"ClientId": "Swagger-UI"
},
@@ -32,8 +37,8 @@
},
"RabbitMQ": {
"Hostname": "localhost",
- "Username": "guest",
- "Password": "guest"
+ "Username": "test",
+ "Password": "test"
}
},
"Logging": {
@@ -41,6 +46,5 @@
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
- }
}
-}
+} }
diff --git a/API/appsettingsapi.json b/API/appsettingsapi.json
index af29dc99..f5d623dd 100644
--- a/API/appsettingsapi.json
+++ b/API/appsettingsapi.json
@@ -14,6 +14,12 @@
"ClientId": "",
"ClientSecret": ""
},
+ "Elastic": {
+ "Hostname": "",
+ "Username": "",
+ "Password": "",
+ "IndexUrl": ""
+ },
"Swagger": {
"ClientId": "Swagger-UI"
@@ -31,9 +37,9 @@
}
},
"RabbitMQ": {
- "Hostname": "localhost",
- "Username": "guest",
- "Password": "guest"
+ "Hostname": "",
+ "Username": "",
+ "Password": ""
}
},
"Logging": {
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a4d41ea..98385ce9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,22 +9,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
-
### Changed
-
### Deprecated
-
### Removed
-
### Fixed
-
### Security
+## Release v.1.2.0-beta - 15-04-2021
+
+### Added
+
+- Added tests for the wizard - [#372](https://github.com/DigitalExcellence/dex-backend/issues/372)
+- Recommendation system, allowing users to get projects recommended based on similar users [#63](https://github.com/DigitalExcellence/dex-backend/issues/63)
+- Categories. It is now possible to categorize projects. [#362](https://github.com/DigitalExcellence/dex-backend/issues/362)
+
+### Changed
+- Improved integration tests in pipeline - [#395](https://github.com/DigitalExcellence/dex-backend/issues/395)
+
+### Fixed
+- Renamed IdentityServer to match the rest of the project name - [#386](https://github.com/DigitalExcellence/dex-backend/issues/386)
+
+
## Release v.1.1.0-beta - 18-03-2021
### Added
diff --git a/Data/06_Data.csproj b/Data/06_Data.csproj
index d5872299..8d0904f8 100644
--- a/Data/06_Data.csproj
+++ b/Data/06_Data.csproj
@@ -3,7 +3,7 @@
netcoreapp3.1
8
- 1.1.0-beta
+ 1.2.0-beta
@@ -21,6 +21,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/Data/ApplicationDbContext.cs b/Data/ApplicationDbContext.cs
index fbf1b7b5..171795be 100644
--- a/Data/ApplicationDbContext.cs
+++ b/Data/ApplicationDbContext.cs
@@ -95,6 +95,14 @@ public ApplicationDbContext(DbContextOptions options) : ba
///
public DbSet Role { get; set; }
+ ///
+ /// Gets or sets the category.
+ ///
+ ///
+ /// The category.
+ ///
+ public DbSet Category { get; set; }
+
///
/// Gets or sets the institution.
///
diff --git a/Data/ElasticSearchContext.cs b/Data/ElasticSearchContext.cs
new file mode 100644
index 00000000..35bb9949
--- /dev/null
+++ b/Data/ElasticSearchContext.cs
@@ -0,0 +1,43 @@
+using RestSharp;
+using RestSharp.Authenticators;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+
+namespace Data
+{
+ public interface IElasticSearchContext
+ {
+ RestClient CreateRestClientForElasticRequests();
+ }
+ public class ElasticSearchContext : IElasticSearchContext
+ {
+ private string hostname;
+ private string username;
+ private string password;
+ private string indexUrl;
+
+ public ElasticSearchContext(string hostname, string username, string password, string indexUrl)
+ {
+ this.hostname = hostname;
+ this.username = username;
+ this.password = password;
+ this.indexUrl = indexUrl;
+ }
+
+ public RestClient CreateRestClientForElasticRequests()
+ {
+ UriBuilder builder = new UriBuilder("http://" + hostname + ":9200/" + indexUrl);
+
+ Uri uri = builder.Uri;
+ RestClient restClient = new RestClient(uri)
+ {
+ Authenticator =
+ new HttpBasicAuthenticator(username, password)
+ };
+ return restClient;
+ }
+ }
+}
diff --git a/Data/Helpers/Seed.cs b/Data/Helpers/Seed.cs
index 203a4af4..15a9a523 100644
--- a/Data/Helpers/Seed.cs
+++ b/Data/Helpers/Seed.cs
@@ -105,7 +105,8 @@ public static List SeedRoles()
Scopes = new List
{
- new RoleScope(nameof(Defaults.Scopes.ProjectWrite))
+ new RoleScope(nameof(Defaults.Scopes.ProjectWrite)),
+ new RoleScope(nameof(Defaults.Scopes.CategoryRead))
}
};
roles.Add(registeredUserRole);
@@ -119,7 +120,8 @@ public static List SeedRoles()
new RoleScope(nameof(Defaults.Scopes.EmbedWrite)),
new RoleScope(nameof(Defaults.Scopes.HighlightRead)),
new RoleScope(nameof(Defaults.Scopes.HighlightWrite)),
- new RoleScope(nameof(Defaults.Scopes.ProjectWrite))
+ new RoleScope(nameof(Defaults.Scopes.ProjectWrite)),
+ new RoleScope(nameof(Defaults.Scopes.CategoryRead))
}
};
roles.Add(prRole);
@@ -133,6 +135,7 @@ public static List SeedRoles()
new RoleScope(nameof(Defaults.Scopes.InstitutionUserWrite)),
new RoleScope(nameof(Defaults.Scopes.InstitutionEmbedWrite)),
new RoleScope(nameof(Defaults.Scopes.InstitutionProjectWrite)),
+ new RoleScope(nameof(Defaults.Scopes.CategoryRead)),
new RoleScope(nameof(Defaults.Scopes.ProjectWrite))
}
};
@@ -148,6 +151,8 @@ public static List SeedRoles()
new RoleScope(nameof(Defaults.Scopes.UserRead)),
new RoleScope(nameof(Defaults.Scopes.RoleRead)),
new RoleScope(nameof(Defaults.Scopes.RoleWrite)),
+ new RoleScope(nameof(Defaults.Scopes.CategoryRead)),
+ new RoleScope(nameof(Defaults.Scopes.CategoryWrite)),
new RoleScope(nameof(Defaults.Scopes.HighlightRead)),
new RoleScope(nameof(Defaults.Scopes.HighlightWrite)),
new RoleScope(nameof(Defaults.Scopes.EmbedRead)),
@@ -157,21 +162,52 @@ public static List SeedRoles()
new RoleScope(nameof(Defaults.Scopes.FileWrite)),
new RoleScope(nameof(Defaults.Scopes.CallToActionOptionWrite)),
new RoleScope(nameof(Defaults.Scopes.ProjectWrite)),
- new RoleScope(nameof(Defaults.Scopes.DataSourceWrite))
+ new RoleScope(nameof(Defaults.Scopes.DataSourceWrite)),
+ new RoleScope(nameof(Defaults.Scopes.WizardPageWrite)),
+ new RoleScope(nameof(Defaults.Scopes.AdminProjectExport))
}
};
roles.Add(administratorRole);
Role alumniRole = new Role
- {
- Name = nameof(Defaults.Roles.Alumni),
- Scopes = new List()
- };
+ {
+ Name = nameof(Defaults.Roles.Alumni),
+ Scopes = new List
+ {
+ new RoleScope(nameof(Defaults.Scopes.CategoryRead))
+ }
+ };
roles.Add(alumniRole);
return roles;
}
+ public static List SeedCategories()
+ {
+ List categories = new List();
+
+ categories.AddRange(new[]{
+ new Category
+ {
+ Name = "Some Category 1"
+ },
+ new Category
+ {
+ Name = "Some Category 2"
+ },
+ new Category
+ {
+ Name = "Some Category 3"
+ },
+ new Category
+ {
+ Name = "Some Category 4"
+ }
+ });
+
+ return categories;
+ }
+
///
/// Seeds the admin user.
///
@@ -345,6 +381,45 @@ public static List SeedCollaborators(List projects)
return collaborators;
}
+ ///
+ /// Seed random ProjectLikes into the database
+ ///
+ public static List SeedLikes(List projects, List users)
+ {
+ List projectLikes = new List();
+ foreach(Project project in projects)
+ {
+ List usersThatLiked = new List();
+ Random random = new Random();
+ int randomLikes = random.Next(5, 15);
+ for(int i = 0; i < randomLikes; i++)
+ {
+ ProjectLike projectLike = new ProjectLike
+ {
+ UserId = project.User.Id,
+ LikedProject = project
+ };
+
+ bool userFound = false;
+ while(!userFound)
+ {
+ int randomUserId = random.Next(0, users.Count);
+ User u = users[randomUserId];
+ if(!usersThatLiked.Contains(u))
+ {
+ projectLike.ProjectLiker = u;
+ usersThatLiked.Add(u);
+ userFound = true;
+ }
+ }
+
+ projectLikes.Add(projectLike);
+ }
+
+ }
+ return projectLikes;
+ }
+
///
/// Seeds the highlights.
///
@@ -411,6 +486,74 @@ public static List SeedCallToActionOptions()
};
}
+ ///
+ /// This method seeds wizard pages.
+ ///
+ /// Returns a list of wizard pages that can be seeded into the database.
+ public static List SeedWizardPages()
+ {
+ return new List
+ {
+ new WizardPage
+ {
+ Name = "Enter your username",
+ Description = "Enter the username you would like to retrieve projects from"
+ },
+ new WizardPage
+ {
+ Name = "Paste the link to your project",
+ Description = "Enter the link to your project that you would like to import"
+ },
+ new WizardPage
+ {
+ Name = "Select the correct project",
+ Description = "Select which project you would like to retrieve"
+ }
+ };
+ }
+
+ public static List SeedDataSources()
+ {
+ return new List
+ {
+ new DataSource
+ {
+ Title = "Github",
+ Guid = "de38e528-1d6d-40e7-83b9-4334c51c19be",
+ IsVisible = true,
+ Description = ""
+ },
+ new DataSource
+ {
+ Title = "Gitlab",
+ Guid = "66de59d4-5db0-4bf8-a9a5-06abe8d3443a",
+ IsVisible = true,
+ Description = ""
+ },
+ new DataSource
+ {
+ Title = "JsFiddle",
+ Guid = "96666870-3afe-44e2-8d62-337d49cf972d",
+ IsVisible = false,
+ Description = ""
+ }
+ };
+ }
+
+ public static User SeedAdminUser2(List roles)
+ {
+ Role adminRole = roles.Find(i => i.Name == nameof(Defaults.Roles.Administrator));
+
+ User user = new User
+ {
+ Role = adminRole,
+ IdentityId = "74489498",
+ Email = "DeXAdmin@email.com",
+ Name = "DeX Admin",
+ };
+
+ return user;
+ }
}
}
diff --git a/Data/Migrations/20210224135630_AddCategories.Designer.cs b/Data/Migrations/20210224135630_AddCategories.Designer.cs
new file mode 100644
index 00000000..25fdebc6
--- /dev/null
+++ b/Data/Migrations/20210224135630_AddCategories.Designer.cs
@@ -0,0 +1,649 @@
+//
+using System;
+using Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace _4_Data.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20210224135630_AddCategories")]
+ partial class AddCategories
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.3")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128)
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ modelBuilder.Entity("Models.CallToAction", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("OptionValue")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("CallToAction");
+ });
+
+ modelBuilder.Entity("Models.CallToActionOption", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("CallToActionOption");
+ });
+
+ modelBuilder.Entity("Models.Collaborator", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("FullName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ProjectId")
+ .HasColumnType("int");
+
+ b.Property("Role")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProjectId");
+
+ b.ToTable("Collaborators");
+ });
+
+ modelBuilder.Entity("Models.DataSource", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Guid")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IconId")
+ .HasColumnType("int");
+
+ b.Property("IsVisible")
+ .HasColumnType("bit");
+
+ b.Property("Title")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IconId");
+
+ b.ToTable("DataSource");
+ });
+
+ modelBuilder.Entity("Models.DataSourceWizardPage", b =>
+ {
+ b.Property("DataSourceId")
+ .HasColumnType("int");
+
+ b.Property("WizardPageId")
+ .HasColumnType("int");
+
+ b.Property("AuthFlow")
+ .HasColumnType("bit");
+
+ b.Property("OrderIndex")
+ .HasColumnType("int");
+
+ b.HasKey("DataSourceId", "WizardPageId", "AuthFlow");
+
+ b.HasIndex("WizardPageId");
+
+ b.ToTable("DataSourceWizardPage");
+ });
+
+ modelBuilder.Entity("Models.EmbeddedProject", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("Guid")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ProjectId")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProjectId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("EmbeddedProject");
+ });
+
+ modelBuilder.Entity("Models.File", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UploadDateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("UploaderId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UploaderId");
+
+ b.ToTable("File");
+ });
+
+ modelBuilder.Entity("Models.Highlight", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("EndDate")
+ .HasColumnType("datetime2");
+
+ b.Property("ProjectId")
+ .HasColumnType("int");
+
+ b.Property("StartDate")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProjectId");
+
+ b.ToTable("Highlight");
+ });
+
+ modelBuilder.Entity("Models.Institution", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IdentityId")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Institution");
+ });
+
+ modelBuilder.Entity("Models.Project", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("CallToActionId")
+ .HasColumnType("int");
+
+ b.Property("Created")
+ .HasColumnType("datetime2");
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("InstitutePrivate")
+ .HasColumnType("bit");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ProjectIconId")
+ .HasColumnType("int");
+
+ b.Property("ShortDescription")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Updated")
+ .HasColumnType("datetime2");
+
+ b.Property("Uri")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CallToActionId");
+
+ b.HasIndex("ProjectIconId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Project");
+ });
+
+ modelBuilder.Entity("Models.ProjectLike", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("Date")
+ .HasColumnType("datetime2");
+
+ b.Property("LikedProjectId")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LikedProjectId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ProjectLike");
+ });
+
+ modelBuilder.Entity("Models.Role", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Role");
+ });
+
+ modelBuilder.Entity("Models.RoleScope", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("RoleId")
+ .HasColumnType("int");
+
+ b.Property("Scope")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("RoleScope");
+ });
+
+ modelBuilder.Entity("Models.Category", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Category");
+ });
+
+ modelBuilder.Entity("Models.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("AccountCreationDate")
+ .HasColumnType("datetime2");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ExpectedGraduationDate")
+ .HasColumnType("datetime2");
+
+ b.Property("IdentityId")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("InstitutionId")
+ .HasColumnType("int");
+
+ b.Property("IsPublic")
+ .HasColumnType("bit");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ProfileUrl")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RoleId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstitutionId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("User");
+ });
+
+ modelBuilder.Entity("Models.UserProject", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("ProjectId")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProjectId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("UserProject");
+ });
+
+ modelBuilder.Entity("Models.UserTask", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("Type")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("UserTask");
+ });
+
+ modelBuilder.Entity("Models.UserUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("FollowedUserId")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("FollowedUserId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("UserUser");
+ });
+
+ modelBuilder.Entity("Models.WizardPage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.ToTable("WizardPage");
+ });
+
+ modelBuilder.Entity("Models.Collaborator", b =>
+ {
+ b.HasOne("Models.Project", null)
+ .WithMany("Collaborators")
+ .HasForeignKey("ProjectId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Models.DataSource", b =>
+ {
+ b.HasOne("Models.File", "Icon")
+ .WithMany()
+ .HasForeignKey("IconId");
+ });
+
+ modelBuilder.Entity("Models.DataSourceWizardPage", b =>
+ {
+ b.HasOne("Models.DataSource", "DataSource")
+ .WithMany("DataSourceWizardPages")
+ .HasForeignKey("DataSourceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Models.WizardPage", "WizardPage")
+ .WithMany("DataSourceWizardPages")
+ .HasForeignKey("WizardPageId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Models.EmbeddedProject", b =>
+ {
+ b.HasOne("Models.Project", "Project")
+ .WithMany()
+ .HasForeignKey("ProjectId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Models.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Models.File", b =>
+ {
+ b.HasOne("Models.User", "Uploader")
+ .WithMany()
+ .HasForeignKey("UploaderId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Models.Highlight", b =>
+ {
+ b.HasOne("Models.Project", "Project")
+ .WithMany()
+ .HasForeignKey("ProjectId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Models.Project", b =>
+ {
+ b.HasOne("Models.CallToAction", "CallToAction")
+ .WithMany()
+ .HasForeignKey("CallToActionId");
+
+ b.HasOne("Models.File", "ProjectIcon")
+ .WithMany()
+ .HasForeignKey("ProjectIconId");
+
+ b.HasOne("Models.User", "User")
+ .WithMany("Projects")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Models.ProjectLike", b =>
+ {
+ b.HasOne("Models.Project", "LikedProject")
+ .WithMany("Likes")
+ .HasForeignKey("LikedProjectId");
+
+ b.HasOne("Models.User", "ProjectLiker")
+ .WithMany("LikedProjectsByUsers")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Models.RoleScope", b =>
+ {
+ b.HasOne("Models.Role", null)
+ .WithMany("Scopes")
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Models.User", b =>
+ {
+ b.HasOne("Models.Institution", "Institution")
+ .WithMany()
+ .HasForeignKey("InstitutionId");
+
+ b.HasOne("Models.Role", "Role")
+ .WithMany()
+ .HasForeignKey("RoleId");
+ });
+
+ modelBuilder.Entity("Models.UserProject", b =>
+ {
+ b.HasOne("Models.Project", "Project")
+ .WithMany()
+ .HasForeignKey("ProjectId");
+
+ b.HasOne("Models.User", "User")
+ .WithMany("UserProject")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Models.UserTask", b =>
+ {
+ b.HasOne("Models.User", "User")
+ .WithMany("UserTasks")
+ .HasForeignKey("UserId");
+ });
+
+ modelBuilder.Entity("Models.UserUser", b =>
+ {
+ b.HasOne("Models.User", "FollowedUser")
+ .WithMany("FollowedUsers")
+ .HasForeignKey("FollowedUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Models.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Data/Migrations/20210224135630_AddCategories.cs b/Data/Migrations/20210224135630_AddCategories.cs
new file mode 100644
index 00000000..4dca7c85
--- /dev/null
+++ b/Data/Migrations/20210224135630_AddCategories.cs
@@ -0,0 +1,47 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace _4_Data.Migrations
+{
+ public partial class AddCategories : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Category",
+ columns: table => new
+ {
+ Id = table.Column(nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ Name = table.Column(nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Category", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "ProjectCategory",
+ columns: table => new
+ {
+ Id = table.Column(nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ ProjectId = table.Column(nullable: false),
+ CategoryId = table.Column(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ProjectCategory", x => x.Id);
+ });
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Category");
+
+ migrationBuilder.DropTable(
+ name: "ProjectCategory");
+ }
+ }
+}
diff --git a/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/Data/Migrations/ApplicationDbContextModelSnapshot.cs
index 75d5ee5c..7f6abc55 100644
--- a/Data/Migrations/ApplicationDbContextModelSnapshot.cs
+++ b/Data/Migrations/ApplicationDbContextModelSnapshot.cs
@@ -1,4 +1,4 @@
-//
+//
using System;
using Data;
using Microsoft.EntityFrameworkCore;
@@ -365,6 +365,21 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.ToTable("RoleScope");
});
+ modelBuilder.Entity("Models.Category", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Category");
+ });
+
modelBuilder.Entity("Models.User", b =>
{
b.Property("Id")
diff --git a/Digital_Excellence.sln b/Digital_Excellence.sln
index c4bcb15f..16b8e896 100644
--- a/Digital_Excellence.sln
+++ b/Digital_Excellence.sln
@@ -22,12 +22,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "06_Data", "Data\06_Data.csp
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "07_Models", "Models\07_Models.csproj", "{22BBD62E-AD77-46E5-86F8-900737BE19FB}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "6_IdentityServer", "IdentityServer\6_IdentityServer.csproj", "{823A7672-639A-4C68-AD71-04C9C62E1468}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "08_IdentityServer", "IdentityServer\08_IdentityServer.csproj", "{823A7672-639A-4C68-AD71-04C9C62E1468}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "09_MessageBrokerPublisher", "MessageBrokerPublisher\09_MessageBrokerPublisher.csproj", "{3A0B6EED-3F6B-43F7-9C1E-0311EC9871CB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "10_MessageBrokerPublisher.Tests", "MessageBrokerPublisher.Tests\10_MessageBrokerPublisher.Tests.csproj", "{4F2E803B-9C59-4A7F-9830-6627381851EA}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "14_ElasticSynchronizer", "ElasticSynchronizer\14_ElasticSynchronizer.csproj", "{E8FB8195-9C72-4366-9F4B-65230EF98031}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "11_NotificationSystem", "NotificationSystem\11_NotificationSystem.csproj", "{A0F1BB66-1749-47B7-940F-2EA6D004F33B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "12_NotificationSystem.Tests", "NotificationSystem.Tests\12_NotificationSystem.Tests.csproj", "{C85546CD-EFA2-42C5-A698-175BAE85515E}"
@@ -80,6 +82,10 @@ Global
{4F2E803B-9C59-4A7F-9830-6627381851EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4F2E803B-9C59-4A7F-9830-6627381851EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4F2E803B-9C59-4A7F-9830-6627381851EA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E8FB8195-9C72-4366-9F4B-65230EF98031}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E8FB8195-9C72-4366-9F4B-65230EF98031}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E8FB8195-9C72-4366-9F4B-65230EF98031}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E8FB8195-9C72-4366-9F4B-65230EF98031}.Release|Any CPU.Build.0 = Release|Any CPU
{A0F1BB66-1749-47B7-940F-2EA6D004F33B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A0F1BB66-1749-47B7-940F-2EA6D004F33B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0F1BB66-1749-47B7-940F-2EA6D004F33B}.Release|Any CPU.ActiveCfg = Release|Any CPU
diff --git a/ElasticSynchronizer/14_ElasticSynchronizer.csproj b/ElasticSynchronizer/14_ElasticSynchronizer.csproj
new file mode 100644
index 00000000..0cddb968
--- /dev/null
+++ b/ElasticSynchronizer/14_ElasticSynchronizer.csproj
@@ -0,0 +1,18 @@
+
+
+
+ netcoreapp3.1
+ dotnet-ElasticSynchronizer-03111C2E-844A-4534-8446-C60B5B11289B
+ 1.2.0-beta
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ElasticSynchronizer/Configuration/Config.cs b/ElasticSynchronizer/Configuration/Config.cs
new file mode 100644
index 00000000..915d7c67
--- /dev/null
+++ b/ElasticSynchronizer/Configuration/Config.cs
@@ -0,0 +1,54 @@
+/*
+* Digital Excellence Copyright (C) 2020 Brend Smits
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Lesser General Public License as published
+* by the Free Software Foundation version 3 of the License.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty
+* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+* See the GNU Lesser General Public License for more details.
+*
+* You can find a copy of the GNU Lesser General Public License
+* along with this program, in the LICENSE.md file in the root project directory.
+* If not, see https://www.gnu.org/licenses/lgpl-3.0.txt
+*/
+
+using NetEscapades.Configuration.Validation;
+using System.ComponentModel.DataAnnotations;
+
+namespace ElasticSynchronizer.Configuration
+{
+
+ public class Config : IValidatable
+ {
+
+ ///
+ /// Gets or sets the Elastic config.
+ ///
+ ///
+ /// The Elastic config.
+ ///
+ public ElasticConfig Elastic { get; set; }
+
+ ///
+ /// Gets or sets the rabbit mq config.
+ ///
+ ///
+ /// The rabbit mq config.
+ ///
+ public RabbitMQConfig RabbitMQ { get; set; }
+
+ ///
+ /// Validates this instance.
+ ///
+ public void Validate()
+ {
+ Validator.ValidateObject(RabbitMQ, new ValidationContext(RabbitMQ), true);
+ Validator.ValidateObject(Elastic, new ValidationContext(Elastic), true);
+ }
+
+ }
+
+}
diff --git a/ElasticSynchronizer/Configuration/ElasticConfig.cs b/ElasticSynchronizer/Configuration/ElasticConfig.cs
new file mode 100644
index 00000000..55af317e
--- /dev/null
+++ b/ElasticSynchronizer/Configuration/ElasticConfig.cs
@@ -0,0 +1,65 @@
+/*
+* Digital Excellence Copyright (C) 2020 Brend Smits
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Lesser General Public License as published
+* by the Free Software Foundation version 3 of the License.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty
+* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+* See the GNU Lesser General Public License for more details.
+*
+* You can find a copy of the GNU Lesser General Public License
+* along with this program, in the LICENSE.md file in the root project directory.
+* If not, see https://www.gnu.org/licenses/lgpl-3.0.txt
+*/
+
+using System.ComponentModel.DataAnnotations;
+
+namespace ElasticSynchronizer.Configuration
+{
+
+ public class ElasticConfig
+ {
+
+ ///
+ /// Gets or sets the hostname.
+ ///
+ ///
+ /// The hostname of the elastic instance.
+ ///
+ [Required]
+ public string Hostname { get; set; }
+
+ ///
+ /// Gets or sets the username.
+ ///
+ ///
+ /// The username of the elastic instance.
+ ///
+ [Required]
+ public string Username { get; set; }
+
+ ///
+ /// Gets or sets the password.
+ ///
+ ///
+ /// The password of the elastic instance.
+ ///
+ [Required]
+ public string Password { get; set; }
+
+
+ ///
+ /// Gets or sets the index.
+ ///
+ ///
+ /// The index of the elastic document.
+ ///
+ [Required]
+ public string IndexUrl { get; set; }
+
+ }
+
+}
diff --git a/ElasticSynchronizer/Configuration/RabbitMQConfig.cs b/ElasticSynchronizer/Configuration/RabbitMQConfig.cs
new file mode 100644
index 00000000..a699df54
--- /dev/null
+++ b/ElasticSynchronizer/Configuration/RabbitMQConfig.cs
@@ -0,0 +1,55 @@
+/*
+* Digital Excellence Copyright (C) 2020 Brend Smits
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Lesser General Public License as published
+* by the Free Software Foundation version 3 of the License.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty
+* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+* See the GNU Lesser General Public License for more details.
+*
+* You can find a copy of the GNU Lesser General Public License
+* along with this program, in the LICENSE.md file in the root project directory.
+* If not, see https://www.gnu.org/licenses/lgpl-3.0.txt
+*/
+
+using System.ComponentModel.DataAnnotations;
+
+namespace ElasticSynchronizer.Configuration
+{
+
+ public class RabbitMQConfig
+ {
+
+ ///
+ /// Gets or sets the hostname.
+ ///
+ ///
+ /// The hostname of the rabbit mq instance.
+ ///
+ [Required]
+ public string Hostname { get; set; }
+
+ ///
+ /// Gets or sets the username.
+ ///
+ ///
+ /// The username of the rabbit mq instance.
+ ///
+ [Required]
+ public string Username { get; set; }
+
+ ///
+ /// Gets or sets the password.
+ ///
+ ///
+ /// The password of the rabbit mq instance.
+ ///
+ [Required]
+ public string Password { get; set; }
+
+ }
+
+}
diff --git a/ElasticSynchronizer/Dockerfile b/ElasticSynchronizer/Dockerfile
new file mode 100644
index 00000000..b7137920
--- /dev/null
+++ b/ElasticSynchronizer/Dockerfile
@@ -0,0 +1,15 @@
+FROM mcr.microsoft.com/dotnet/sdk:3.1 AS build-env
+
+WORKDIR /app
+
+COPY . ./
+
+RUN dotnet publish ElasticSynchronizer/14_ElasticSynchronizer.csproj -c Release -o out
+
+FROM mcr.microsoft.com/dotnet/aspnet:3.1
+
+WORKDIR /app
+
+COPY --from=build-env /app/out .
+
+ENTRYPOINT ["dotnet", "14_ElasticSynchronizer.dll"]
diff --git a/ElasticSynchronizer/Executors/DocumentDeleter.cs b/ElasticSynchronizer/Executors/DocumentDeleter.cs
new file mode 100644
index 00000000..5caf8534
--- /dev/null
+++ b/ElasticSynchronizer/Executors/DocumentDeleter.cs
@@ -0,0 +1,98 @@
+/*
+* Digital Excellence Copyright (C) 2020 Brend Smits
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Lesser General Public License as published
+* by the Free Software Foundation version 3 of the License.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty
+* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+* See the GNU Lesser General Public License for more details.
+*
+* You can find a copy of the GNU Lesser General Public License
+* along with this program, in the LICENSE.md file in the root project directory.
+* If not, see https://www.gnu.org/licenses/lgpl-3.0.txt
+*/
+
+using ElasticSynchronizer.Configuration;
+using ElasticSynchronizer.Models;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using NotificationSystem.Contracts;
+using RestSharp;
+using Serilog;
+using System;
+
+namespace ElasticSynchronizer.Executors
+{
+ ///
+ /// This class is used as a callback to be passed into the RabbitMQ Consumer.
+ ///
+ public class DocumentDeleter : ICallbackService
+ {
+
+ private readonly Config config;
+ private readonly RestClient restClient;
+ private ESProjectDTO eSProject;
+
+ public DocumentDeleter(Config config, RestClient restClient)
+ {
+ this.config = config;
+ this.restClient = restClient;
+ }
+
+ ///
+ /// Parses the payload.
+ ///
+ public void ParsePayload(string jsonBody)
+ {
+ try
+ {
+ Log.Logger.Information("Document deleter");
+ Log.Logger.Information("Json payload: " + jsonBody);
+ eSProject = JsonConvert.DeserializeObject(jsonBody);
+ } catch (Exception e)
+ {
+ Log.Logger.Error("Failed: " + e.Message );
+ Log.Logger.Information(jsonBody);
+ }
+
+ }
+
+ ///
+ /// Executes the DeleteDocument Method.
+ ///
+ public void ExecuteTask()
+ {
+ DeleteDocument();
+ }
+
+ ///
+ /// Validates the payload.
+ ///
+ public bool ValidatePayload()
+ {
+ if(eSProject.Id <= 0)
+ {
+ throw new Exception("Invalid Project Id");
+ }
+ return true;
+ }
+
+ ///
+ /// Sends API Call to the ElasticSearch Index, requesting the deletion of given Project.
+ ///
+ private void DeleteDocument()
+ {
+ RestRequest request = new RestRequest(config.Elastic.IndexUrl + "_doc/" + eSProject.Id, Method.DELETE);
+ IRestResponse response = restClient.Execute(request);
+ if(!response.IsSuccessful)
+ {
+ Log.Logger.Error("Failed: " + response.StatusDescription + response.StatusCode + response.Content);
+ }
+ }
+
+ }
+
+}
diff --git a/ElasticSynchronizer/Executors/DocumentUpdater.cs b/ElasticSynchronizer/Executors/DocumentUpdater.cs
new file mode 100644
index 00000000..e0e33405
--- /dev/null
+++ b/ElasticSynchronizer/Executors/DocumentUpdater.cs
@@ -0,0 +1,101 @@
+/*
+* Digital Excellence Copyright (C) 2020 Brend Smits
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Lesser General Public License as published
+* by the Free Software Foundation version 3 of the License.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty
+* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+* See the GNU Lesser General Public License for more details.
+*
+* You can find a copy of the GNU Lesser General Public License
+* along with this program, in the LICENSE.md file in the root project directory.
+* If not, see https://www.gnu.org/licenses/lgpl-3.0.txt
+*/
+
+using ElasticSynchronizer.Configuration;
+using ElasticSynchronizer.Models;
+using Newtonsoft.Json;
+using NotificationSystem.Contracts;
+using RestSharp;
+using Serilog;
+using System;
+
+namespace ElasticSynchronizer.Executors
+{
+ ///
+ /// This class is used as a callback to be passed into the RabbitMQ Consumer.
+ ///
+ public class DocumentUpdater : ICallbackService
+ {
+ private readonly Config config;
+ private readonly RestClient restClient;
+ private ESProjectDTO eSProject;
+
+ public DocumentUpdater(Config config, RestClient restClient)
+ {
+ this.config = config;
+ this.restClient = restClient;
+ }
+
+ ///
+ /// Parses the payload.
+ ///
+ public void ParsePayload(string jsonBody)
+ {
+ try
+ {
+ Log.Logger.Information("Document updater");
+ Log.Logger.Information("Json payload: " + jsonBody);
+ eSProject = JsonConvert.DeserializeObject(jsonBody);
+ } catch(Exception e)
+ {
+ Log.Logger.Error("Failed: " + e.Message);
+ Log.Logger.Information(jsonBody);
+ }
+ }
+
+
+ ///
+ /// Executes the CreateOrUpdateDocument method.
+ ///
+ public void ExecuteTask()
+ {
+ CreateOrUpdateDocument();
+ }
+
+
+ ///
+ /// Validates the payload.
+ ///
+ public bool ValidatePayload()
+ {
+ if(eSProject.Id <= 0)
+ {
+ throw new Exception("Invalid Project Id");
+ }
+ return true;
+ }
+
+ ///
+ /// Sends API Call to the ElasticSearch Index, requesting the creation or update of given Project.
+ ///
+ private void CreateOrUpdateDocument()
+ {
+ string body = JsonConvert.SerializeObject(eSProject);
+ RestRequest request = new RestRequest(config.Elastic.IndexUrl + "_doc/" + eSProject.Id, Method.PUT);
+ request.AddParameter("application/json", body, ParameterType.RequestBody);
+
+ IRestResponse response = restClient.Execute(request);
+
+ if(!response.IsSuccessful)
+ {
+ Log.Logger.Error("Failed: " + response.StatusDescription + response.StatusCode + response.Content);
+ }
+ }
+
+ }
+
+}
diff --git a/ElasticSynchronizer/Helperclasses/JsonParser.cs b/ElasticSynchronizer/Helperclasses/JsonParser.cs
new file mode 100644
index 00000000..36d6ba0b
--- /dev/null
+++ b/ElasticSynchronizer/Helperclasses/JsonParser.cs
@@ -0,0 +1,34 @@
+using ElasticSynchronizer.Models;
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections.Generic;
+
+namespace ElasticSynchronizer.Helperclasses
+{
+
+ public interface IJsonParser
+ {
+
+ ESProjectDTO JsonStringToProjectES(string body);
+
+ }
+
+ public class JsonParser : IJsonParser
+ {
+
+ public ESProjectDTO JsonStringToProjectES(string body)
+ {
+ JToken token = JToken.Parse(body);
+ ESProjectDTO project = new ESProjectDTO();
+ project.Description = token.Value("Description");
+ project.ProjectName = token.Value("Name");
+ project.Id = token.Value("Id");
+ project.Created = token.Value("Created");
+ project.Likes = token.Value>("DescLikesription");
+
+ return project;
+ }
+
+ }
+
+}
diff --git a/ElasticSynchronizer/Models/ESProjectDTO.cs b/ElasticSynchronizer/Models/ESProjectDTO.cs
new file mode 100644
index 00000000..f05dba90
--- /dev/null
+++ b/ElasticSynchronizer/Models/ESProjectDTO.cs
@@ -0,0 +1,57 @@
+/*
+* Digital Excellence Copyright (C) 2020 Brend Smits
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Lesser General Public License as published
+* by the Free Software Foundation version 3 of the License.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty
+* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+* See the GNU Lesser General Public License for more details.
+*
+* You can find a copy of the GNU Lesser General Public License
+* along with this program, in the LICENSE.md file in the root project directory.
+* If not, see https://www.gnu.org/licenses/lgpl-3.0.txt
+*/
+
+using System;
+using System.Collections.Generic;
+
+namespace ElasticSynchronizer.Models
+{
+
+ ///
+ /// This model is the model used to create, update or delete documents in Elastic Search.
+ ///
+ public class ESProjectDTO
+ {
+
+ ///
+ /// This gets or sets the created date of the project
+ ///
+ public DateTime Created { get; set; }
+
+ ///
+ /// This gets or sets the Id of the project
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// This gets or sets the name of the project
+ ///
+ public string ProjectName { get; set; }
+
+ ///
+ /// This gets or sets the description of the project
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// This gets or sets the likes of the project. Likes are stored as an array of user id's who like the project.
+ ///
+ public List Likes { get; set; }
+
+ }
+
+}
diff --git a/ElasticSynchronizer/Program.cs b/ElasticSynchronizer/Program.cs
new file mode 100644
index 00000000..730aa91e
--- /dev/null
+++ b/ElasticSynchronizer/Program.cs
@@ -0,0 +1,90 @@
+/*
+* Digital Excellence Copyright (C) 2020 Brend Smits
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Lesser General Public License as published
+* by the Free Software Foundation version 3 of the License.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty
+* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+* See the GNU Lesser General Public License for more details.
+*
+* You can find a copy of the GNU Lesser General Public License
+* along with this program, in the LICENSE.md file in the root project directory.
+* If not, see https://www.gnu.org/licenses/lgpl-3.0.txt
+*/
+
+using ElasticSynchronizer.Configuration;
+using ElasticSynchronizer.Workers;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using RestSharp;
+using RestSharp.Authenticators;
+using Serilog;
+using Serilog.Events;
+using Serilog.Sinks.SystemConsole.Themes;
+using System;
+
+namespace ElasticSynchronizer
+{
+
+ public class Program
+ {
+
+ public static void Main(string[] args)
+ {
+ Log.Logger = new LoggerConfiguration()
+ .MinimumLevel.Debug()
+ .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
+ .MinimumLevel.Override("System", LogEventLevel.Warning)
+ .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Information)
+ .Enrich.FromLogContext()
+ .WriteTo.Console(outputTemplate:
+ "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}",
+ theme: AnsiConsoleTheme.Literate)
+ .CreateLogger();
+
+ CreateHostBuilder(args)
+ .UseSerilog()
+ .Build()
+ .Run();
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args)
+ {
+ return Host.CreateDefaultBuilder(args)
+ .ConfigureServices((hostContext, services) =>
+ {
+ string environmentName = Environment.GetEnvironmentVariable("ELASTIC_DOTNET_ENVIRONMENT");
+
+ IConfiguration configuration = new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json", true, true)
+ .AddJsonFile($"appsettings.{environmentName}.json",
+ true,
+ true)
+ .AddEnvironmentVariables()
+ .Build();
+
+ Config config = configuration.GetSection("App")
+ .Get();
+ UriBuilder builder = new UriBuilder("http://" + config.Elastic.Hostname + ":9200/");
+ Uri uri = builder.Uri;
+
+ services.AddScoped(c => config);
+ services.AddScoped(client => new RestClient(uri)
+ {
+ Authenticator =
+ new HttpBasicAuthenticator(config.Elastic.Username,
+ config.Elastic.Password)
+ });
+
+ services.AddHostedService();
+ services.AddHostedService();
+ });
+ }
+
+ }
+
+}
diff --git a/ElasticSynchronizer/Properties/launchSettings.json b/ElasticSynchronizer/Properties/launchSettings.json
new file mode 100644
index 00000000..69bc895c
--- /dev/null
+++ b/ElasticSynchronizer/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "ElasticSynchronizer": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ELASTIC_DOTNET_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/ElasticSynchronizer/Workers/DeleteProjectWorker.cs b/ElasticSynchronizer/Workers/DeleteProjectWorker.cs
new file mode 100644
index 00000000..afa41c54
--- /dev/null
+++ b/ElasticSynchronizer/Workers/DeleteProjectWorker.cs
@@ -0,0 +1,66 @@
+/*
+* Digital Excellence Copyright (C) 2020 Brend Smits
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Lesser General Public License as published
+* by the Free Software Foundation version 3 of the License.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty
+* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+* See the GNU Lesser General Public License for more details.
+*
+* You can find a copy of the GNU Lesser General Public License
+* along with this program, in the LICENSE.md file in the root project directory.
+* If not, see https://www.gnu.org/licenses/lgpl-3.0.txt
+*/
+
+using ElasticSynchronizer.Configuration;
+using ElasticSynchronizer.Executors;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using NotificationSystem.Contracts;
+using NotificationSystem.Services;
+using RabbitMQ.Client;
+using RabbitMQ.Client.Events;
+using RestSharp;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace ElasticSynchronizer.Workers
+{
+
+ public class DeleteProjectWorker : BackgroundService
+ {
+
+ private readonly Config config;
+ private readonly ILogger logger;
+ private readonly RestClient restClient;
+ private readonly string subject = "ELASTIC_DELETE";
+
+
+ public DeleteProjectWorker(ILogger logger, Config config, RestClient restClient)
+ {
+ this.logger = logger;
+ this.config = config;
+ this.restClient = restClient;
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ RabbitMQSubscriber subscriber = new RabbitMQSubscriber(
+ new RabbitMQConnectionFactory(config.RabbitMQ.Hostname,
+ config.RabbitMQ.Username,
+ config.RabbitMQ.Password));
+ IModel channel = subscriber.SubscribeToSubject(subject);
+ RabbitMQListener listener = new RabbitMQListener(channel);
+
+ ICallbackService documentDeleterService = new DocumentDeleter(config, restClient);
+ EventingBasicConsumer consumer = listener.CreateConsumer(documentDeleterService);
+
+ listener.StartConsumer(consumer, subject);
+ }
+
+ }
+
+}
diff --git a/ElasticSynchronizer/Workers/UpdateProjectWorker.cs b/ElasticSynchronizer/Workers/UpdateProjectWorker.cs
new file mode 100644
index 00000000..5ba1f213
--- /dev/null
+++ b/ElasticSynchronizer/Workers/UpdateProjectWorker.cs
@@ -0,0 +1,66 @@
+/*
+* Digital Excellence Copyright (C) 2020 Brend Smits
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Lesser General Public License as published
+* by the Free Software Foundation version 3 of the License.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty
+* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+* See the GNU Lesser General Public License for more details.
+*
+* You can find a copy of the GNU Lesser General Public License
+* along with this program, in the LICENSE.md file in the root project directory.
+* If not, see https://www.gnu.org/licenses/lgpl-3.0.txt
+*/
+
+using ElasticSynchronizer.Configuration;
+using ElasticSynchronizer.Executors;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using NotificationSystem.Contracts;
+using NotificationSystem.Services;
+using RabbitMQ.Client;
+using RabbitMQ.Client.Events;
+using RestSharp;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace ElasticSynchronizer.Workers
+{
+
+ public class UpdateProjectWorker : BackgroundService
+ {
+
+ private readonly Config config;
+ private readonly ILogger logger;
+ private readonly RestClient restClient;
+ private readonly string subject = "ELASTIC_CREATE_OR_UPDATE";
+
+ public UpdateProjectWorker(ILogger logger, Config config, RestClient restClient)
+ {
+ this.logger = logger;
+ this.config = config;
+ this.restClient = restClient;
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ RabbitMQSubscriber subscriber = new RabbitMQSubscriber(
+ new RabbitMQConnectionFactory(config.RabbitMQ.Hostname,
+ config.RabbitMQ.Username,
+ config.RabbitMQ.Password));
+ IModel channel = subscriber.SubscribeToSubject(subject);
+ RabbitMQListener listener = new RabbitMQListener(channel);
+
+
+ ICallbackService documentUpdaterService = new DocumentUpdater(config, restClient);
+ EventingBasicConsumer consumer = listener.CreateConsumer(documentUpdaterService);
+
+ listener.StartConsumer(consumer, subject);
+ }
+
+ }
+
+}
diff --git a/ElasticSynchronizer/appsettings.Development.json b/ElasticSynchronizer/appsettings.Development.json
new file mode 100644
index 00000000..55fa7b04
--- /dev/null
+++ b/ElasticSynchronizer/appsettings.Development.json
@@ -0,0 +1,23 @@
+{
+ "App": {
+ "RabbitMQ": {
+ "Hostname": "localhost",
+ "Username": "test",
+ "Password": "test"
+ },
+ "Elastic": {
+ "Hostname": "localhost",
+ "Username": "elastic",
+ "Password": "changeme",
+ "IndexUrl": "projects/"
+ }
+ },
+
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/ElasticSynchronizer/appsettings.json b/ElasticSynchronizer/appsettings.json
new file mode 100644
index 00000000..188978fa
--- /dev/null
+++ b/ElasticSynchronizer/appsettings.json
@@ -0,0 +1,23 @@
+{
+ "App": {
+ "RabbitMQ": {
+ "Hostname": "",
+ "Username": "",
+ "Password": ""
+ },
+ "Elastic": {
+ "Hostname": "",
+ "Username": "",
+ "Password": "",
+ "IndexUrl": ""
+ }
+ },
+
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/IdentityServer/6_IdentityServer.csproj b/IdentityServer/08_IdentityServer.csproj
similarity index 97%
rename from IdentityServer/6_IdentityServer.csproj
rename to IdentityServer/08_IdentityServer.csproj
index 659aead9..e7cbd3f5 100644
--- a/IdentityServer/6_IdentityServer.csproj
+++ b/IdentityServer/08_IdentityServer.csproj
@@ -3,7 +3,7 @@
netcoreapp3.1
8
- 1.1.0-beta
+ 1.2.0-beta
diff --git a/IdentityServer/Configuration/IdentityConfig.cs b/IdentityServer/Configuration/IdentityConfig.cs
index 89347bfc..a9dfb33d 100644
--- a/IdentityServer/Configuration/IdentityConfig.cs
+++ b/IdentityServer/Configuration/IdentityConfig.cs
@@ -1,16 +1,16 @@
/*
* Digital Excellence Copyright (C) 2020 Brend Smits
-*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU Lesser General Public License as published
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation version 3 of the License.
-*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty
-* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty
+* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
-*
-* You can find a copy of the GNU Lesser General Public License
+*
+* You can find a copy of the GNU Lesser General Public License
* along with this program, in the LICENSE.md file in the root project directory.
* If not, see https://www.gnu.org/licenses/lgpl-3.0.txt
*/
@@ -41,12 +41,15 @@ public static class IdentityConfig
new Scope(nameof(Defaults.Scopes.ProjectWrite)),
new Scope(nameof(Defaults.Scopes.UserWrite)),
new Scope(nameof(Defaults.Scopes.UserRead)),
+ new Scope(nameof(Defaults.Scopes.CategoryWrite)),
+ new Scope(nameof(Defaults.Scopes.CategoryRead)),
new Scope(nameof(Defaults.Scopes.HighlightWrite)),
new Scope(nameof(Defaults.Scopes.HighlightRead)),
new Scope(nameof(Defaults.Scopes.EmbedWrite)),
new Scope(nameof(Defaults.Scopes.EmbedRead)),
new Scope(nameof(Defaults.Scopes.FileWrite)),
- new Scope(nameof(Defaults.Scopes.UserTaskWrite))
+ new Scope(nameof(Defaults.Scopes.UserTaskWrite)),
+ new Scope(nameof(Defaults.Scopes.WizardPageWrite))
}
},
new ApiResource(IdentityServerConstants.LocalApi.ScopeName)
@@ -101,12 +104,15 @@ public static IEnumerable Clients(Config config)
nameof(Defaults.Scopes.ProjectWrite),
nameof(Defaults.Scopes.UserWrite),
nameof(Defaults.Scopes.UserRead),
+ nameof(Defaults.Scopes.CategoryWrite),
+ nameof(Defaults.Scopes.CategoryRead),
nameof(Defaults.Scopes.HighlightRead),
nameof(Defaults.Scopes.HighlightWrite),
nameof(Defaults.Scopes.EmbedWrite),
nameof(Defaults.Scopes.EmbedRead),
nameof(Defaults.Scopes.FileWrite),
- nameof(Defaults.Scopes.UserTaskWrite)
+ nameof(Defaults.Scopes.UserTaskWrite),
+ nameof(Defaults.Scopes.WizardPageWrite)
},
Claims = new List
{
diff --git a/IdentityServer/Dockerfile b/IdentityServer/Dockerfile
index 3f5ef9af..3d05b4d1 100644
--- a/IdentityServer/Dockerfile
+++ b/IdentityServer/Dockerfile
@@ -42,7 +42,7 @@ ENV SENTRY_DSN = ''
RUN dotnet tool install --tool-path ./ dotnet-certificate-tool --version 2.0.1
-RUN dotnet publish IdentityServer/6_IdentityServer.csproj -c Release -o out
+RUN dotnet publish IdentityServer/08_IdentityServer.csproj -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:3.1
diff --git a/IdentityServer/Dockerfile.dev b/IdentityServer/Dockerfile.dev
index 270c87cd..0ec06282 100644
--- a/IdentityServer/Dockerfile.dev
+++ b/IdentityServer/Dockerfile.dev
@@ -25,4 +25,4 @@ COPY --from=build-env /app/IdentityServer/bin/Debug/netcoreapp3.1 .
COPY --from=build-env /app/IdentityServer/wwwroot ./wwwroot
COPY --from=build-env /app/dex-identity.pfx .
-ENTRYPOINT ["dotnet", "6_IdentityServer.dll", "--environment=Development"]
+ENTRYPOINT ["dotnet", "08_IdentityServer.dll", "--environment=Development"]
diff --git a/IdentityServer/Quickstart/TestUsers.cs b/IdentityServer/Quickstart/TestUsers.cs
index d0abaa51..5787be67 100644
--- a/IdentityServer/Quickstart/TestUsers.cs
+++ b/IdentityServer/Quickstart/TestUsers.cs
@@ -105,6 +105,26 @@ public static List