Skip to content

Commit 3e180fc

Browse files
authored
Merge pull request #410 from bcgov/1.2
Version 1.2
2 parents f8e4eed + d6dc7a7 commit 3e180fc

File tree

110 files changed

+3488
-769
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

110 files changed

+3488
-769
lines changed

README.md

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,109 @@
11
# HMCR
2-
Build a system that contractors can upload their activity for highway maintenance, validate the GPS locations, provide summary reports. The system is to reject invalid uploads.
2+
3+
As part of the Highway Maintenance Contract Renewal (HMCR) process the business area defined the reporting requirements for the Maintenance Contractors (MCs). The contracts outlined the fields that have to be reported on and the format that it needs to be reported in.
4+
5+
This presents a problem on how to best collect the data being provided by the MCs and ensure its quality, so that it can be put to use for the Program and Ministry needs. Through this project the program wants to meet the need to automate the data gathering, as well as data validation process then capture the successfully validated data in a database which can then be immediately available to HQ and District Offices.
6+
7+
## Prerequisites
8+
9+
- .Net Core 3.1 SDK
10+
- Node.JS v10.0 or newer
11+
- Microsoft SQL Server 2017 or newer
12+
13+
## Dependencies
14+
15+
- Working KeyCloak Realm with BC Gov IDIR and BCeID
16+
- Ministry of Transportation and Infrastructure GeoServer access
17+
- IDIR service account with access to BC Gov BceID WebService
18+
19+
## Local Development
20+
21+
### Configuration
22+
23+
Use the following steps to configure the local development environment
24+
25+
1. Clone the repository
26+
27+
```
28+
git clone https://github.com/bcgov/HMCR.git
29+
```
30+
31+
2. Create the HMR_DEV database in MS SQL Server
32+
33+
- Delete all existing tables
34+
- Run scripts in `database/V01.1` directory
35+
- Apply incremental scripts `(V14.1 to Vxx.x)` in ascending order
36+
- Create the first admin user in `HMR_SYSTEM_USER` table and assign the `SYSTEM_ADMIN` role in the `HMR_USER_ROLE` table
37+
38+
3. Configure API Server settings
39+
40+
- Copy `api/Hmcr.API/appsettigns.json` to `api/Hmcr.API/appsettigns.Development.json`
41+
- Update the placeholder values with real values, eg., replace the `<app-id>` with actual KeyCloak client id in the `{ "JWT": { "Audience": "<app-id>" } }` field
42+
- Update the connection string to match the database
43+
- Make note of or update the port for the API Server in Visual Studio or through the `properties/launchSettings.json` file.
44+
45+
4. Configure Hangfire Server settings
46+
47+
- Copy `api/Hmcr.Hangfire/appsettigns.json` to `api/Hmcr.Hangfire/appsettigns.Development.json`
48+
- Update the placeholder values with real values, eg., replace the `<ServiceAccount:User>` with actual IDIR service account in the `{ "ServiceAccount": { "User": "<ServiceAccount:User>" } }` field
49+
- Update the connection string to match the database
50+
51+
5. Configure the React development settings
52+
53+
- Create the `client/.env.development.local` file and add the following content
54+
55+
```
56+
# use port value from step 3
57+
REACT_APP_API_HOST=http://localhost:<api-port>
58+
59+
REACT_APP_SSO_HOST=https://sso-dev.pathfinder.gov.bc.ca/auth
60+
REACT_APP_SSO_CLIENT=<client-id>
61+
REACT_APP_SSO_REALM=<realm-id>
62+
63+
REACT_APP_DEFAULT_PAGE_SIZE_OPTIONS=25,50,100,200
64+
REACT_APP_DEFAULT_PAGE_SIZE=25
65+
66+
# Optional, default port is 3000
67+
# PORT=3001
68+
```
69+
70+
- Replace the placeholder values
71+
72+
### Run
73+
74+
Use the following steps to run the local development environment
75+
76+
1. Run the API Server
77+
78+
- F5 in Visual Studio
79+
- Or from console
80+
81+
```
82+
cd api/Hmcr.Api
83+
dotnet restore
84+
dotnet build
85+
dotnet run
86+
```
87+
88+
2. Run the Hangfire Server. _It's only neccessary to run the Hangfire Server if debugging Hangfire jobs_
89+
90+
- F5 in Visual Studio
91+
- Or from console
92+
93+
```
94+
cd api/Hmcr.Hangfire
95+
dotnet restore
96+
dotnet build
97+
dotnet run
98+
```
99+
100+
3. Run the React frontend
101+
```
102+
cd client
103+
npm install
104+
npm start
105+
```
106+
107+
## OpenShift Deployment
108+
109+
Refer to [this document](openshift/README.md) for OpenShift Deployment and Pipeline related topics

api/Hmcr.Api/Authentication/HmcrJwtBearerEvents.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ public override async Task TokenValidated(TokenValidatedContext context)
6868

6969
private async Task<bool> PopulateCurrentUserFromDb(ClaimsPrincipal principal)
7070
{
71-
_curentUser.UserName = principal.FindFirstValue(HmcrClaimTypes.KcUsername);
71+
var isApiClient = false;
72+
bool.TryParse(principal.FindFirstValue(HmcrClaimTypes.KcIsApiClient), out isApiClient);
73+
74+
_curentUser.UserName = isApiClient ? principal.FindFirstValue(HmcrClaimTypes.KcApiUsername) : principal.FindFirstValue(HmcrClaimTypes.KcUsername);
7275
var usernames = _curentUser.UserName.Split("@");
7376
var username = usernames[0].ToUpperInvariant();
7477
var directory = usernames[1].ToUpperInvariant();
@@ -105,8 +108,9 @@ private async Task<bool> PopulateCurrentUserFromDb(ClaimsPrincipal principal)
105108
_curentUser.UserName = username;
106109
_curentUser.FirstName = user.FirstName;
107110
_curentUser.LastName = user.LastName;
111+
_curentUser.ApiClientId = user.ApiClientId;
108112

109-
if (user.Username.ToUpperInvariant() != username || user.Email.ToUpperInvariant() != email)
113+
if (!isApiClient && user.Username.ToUpperInvariant() != username || user.Email.ToUpperInvariant() != email)
110114
{
111115
_logger.LogWarning($"Username/Email changed from {user.Username}/{user.Email} to {user.Email}/{email}.");
112116
await _userService.UpdateUserFromBceidAsync(userGuid, username, user.UserType, user.ConcurrencyControlNumber);

api/Hmcr.Api/Controllers/ExportController.cs

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,14 @@ public ExportController(HmcrCurrentUser currentUser, IExportApi exportApi)
3737
/// </summary>
3838
/// <param name="serviceAreas">1 ~ 28</param>
3939
/// <param name="typeName">hmr:HMR_WORK_REPORT_VW, hmr:HMR_WILDLIFE_REPORT_VW, hmr:HMR_ROCKFALL_REPORT_VW</param>
40-
/// <param name="format">csv, application/json, application/vnd.google-earth.kml+xml</param>
40+
/// <param name="format">Supported formats: CSV, JSON, KML, GML</param>
4141
/// <param name="fromDate">From date in yyyy-MM-dd format</param>
4242
/// <param name="toDate">To date in yyyy-MM-dd format</param>
4343
/// <param name="cql_filter">Filter</param>
44+
/// <param name="propertyName">Property names of the columns to export</param>
4445
/// <returns></returns>
4546
[HttpGet("report", Name = "Export")]
46-
[RequiresPermission(Permissions.Export)]
47+
[RequiresPermission(Permissions.Export)]
4748
public async Task<IActionResult> ExportReport(string serviceAreas, string typeName, string format, DateTime fromDate, DateTime toDate, string cql_filter, string propertyName)
4849
{
4950
var serviceAreaNumbers = serviceAreas.ToDecimalArray();
@@ -52,15 +53,15 @@ public async Task<IActionResult> ExportReport(string serviceAreas, string typeNa
5253
{
5354
serviceAreaNumbers = _currentUser.UserInfo.ServiceAreas.Select(x => x.ServiceAreaNumber).ToArray();
5455
}
55-
56+
5657
var invalidResult = ValidateQueryParameters(serviceAreaNumbers, typeName, format, fromDate, toDate);
5758

5859
if (invalidResult != null)
5960
{
6061
return invalidResult;
6162
}
6263

63-
var (mimeType, fileName, outputFormat) = GetContentType(format);
64+
var (mimeType, fileName, outputFormat, endpointConfigName) = GetContentType(format);
6465

6566
if (mimeType == null)
6667
{
@@ -70,7 +71,7 @@ public async Task<IActionResult> ExportReport(string serviceAreas, string typeNa
7071

7172
var dateColName = GetDateColName(typeName);
7273

73-
var (result, exists) = await MatchExists(serviceAreaNumbers, fromDate, toDate, outputFormat, dateColName);
74+
var (result, exists) = await MatchExists(serviceAreaNumbers, fromDate, toDate, "text/csv;charset=UTF-8", dateColName, ExportQueryEndpointConfigName.WFS);
7475

7576
if (result != null)
7677
{
@@ -82,9 +83,9 @@ public async Task<IActionResult> ExportReport(string serviceAreas, string typeNa
8283
return NotFound();
8384
}
8485

85-
var query = BuildQuery(serviceAreaNumbers, fromDate, toDate, outputFormat, dateColName, false);
86+
var query = BuildQuery(serviceAreaNumbers, fromDate, toDate, outputFormat, dateColName, endpointConfigName, false);
8687

87-
var responseMessage = await _exportApi.ExportReport(query);
88+
var responseMessage = await _exportApi.ExportReport(query, endpointConfigName);
8889

8990
var bytes = await responseMessage.Content.ReadAsByteArrayAsync();
9091

@@ -103,19 +104,20 @@ public async Task<IActionResult> ExportReport(string serviceAreas, string typeNa
103104
/// csv: csv format
104105
/// json: geo-json format
105106
/// kml: kml format
106-
/// gml: gml foramt
107+
/// gml: gml format
107108
/// </summary>
108109
/// <returns></returns>
109110
[HttpGet("supportedformats", Name = "SupportedFormats")]
111+
[ProducesResponseType(typeof(OutputFormatDto), 200)]
110112
public IActionResult GetSupportedFormats()
111113
{
112114
return Ok(OutputFormatDto.GetSupportedFormats());
113115
}
114116

115-
private async Task<(UnprocessableEntityObjectResult result, bool exists)> MatchExists(decimal[] serviceAreaNumbers, DateTime fromDate, DateTime toDate, string outputFormat, string dateColName)
117+
private async Task<(UnprocessableEntityObjectResult result, bool exists)> MatchExists(decimal[] serviceAreaNumbers, DateTime fromDate, DateTime toDate, string outputFormat, string dateColName, string endpointConfigName)
116118
{
117-
var query = BuildQuery(serviceAreaNumbers, fromDate, toDate, outputFormat, dateColName, true);
118-
var responseMessage = await _exportApi.ExportReport(query);
119+
var query = BuildQuery(serviceAreaNumbers, fromDate, toDate, outputFormat, dateColName, ExportQueryEndpointConfigName.WFS, true);
120+
var responseMessage = await _exportApi.ExportReport(query, endpointConfigName);
119121

120122
if (responseMessage.StatusCode != HttpStatusCode.OK)
121123
{
@@ -174,31 +176,31 @@ private UnprocessableEntityObjectResult ValidateQueryParameters(decimal[] servic
174176
return null;
175177
}
176178

177-
private (string mimeType, string fileName, string format) GetContentType(string outputFormat)
179+
private (string mimeType, string fileName, string format, string endpointConfigName) GetContentType(string outputFormat)
178180
{
179181
if (outputFormat == null)
180182
{
181-
return (null, null, null);
183+
return (null, null, null, null);
182184
}
183185

184186
outputFormat = outputFormat.ToLowerInvariant();
185187

186188
switch (outputFormat)
187189
{
188190
case OutputFormatDto.Csv:
189-
return ("text/csv;charset=UTF-8", "export.csv", "csv");
191+
return ("text/csv;charset=UTF-8", "export.csv", "csv", ExportQueryEndpointConfigName.WFS);
190192
case OutputFormatDto.Json:
191-
return ("application/json;charset=UTF-8", "export.json", "application/json");
193+
return ("application/json;charset=UTF-8", "export.json", "application/json", ExportQueryEndpointConfigName.WFS);
192194
case OutputFormatDto.Kml:
193-
return ("application/vnd.google-earth.kml+xml;charset=UTF-8", "export.kml", "application/vnd.google-earth.kml+xml");
195+
return ("application/vnd.google-earth.kml+xml;charset=UTF-8", "export.kml", "application/vnd.google-earth.kml+xml", ExportQueryEndpointConfigName.WMS);
194196
case OutputFormatDto.Gml:
195-
return ("application/gml+xml; version=3.2;charset=UTF-8", "export.gml", "application/gml+xml; version=3.2");
197+
return ("application/gml+xml; version=3.2;charset=UTF-8", "export.gml", "application/gml+xml; version=3.2", ExportQueryEndpointConfigName.WFS);
196198
default:
197-
return (null, null, null);
199+
return (null, null, null, null);
198200
}
199201
}
200202

201-
private string BuildQuery(decimal[] serviceAreaNumbers, DateTime fromDate, DateTime toDate, string outputFormat, string dateColName, bool count)
203+
private string BuildQuery(decimal[] serviceAreaNumbers, DateTime fromDate, DateTime toDate, string outputFormat, string dateColName, string endpointConfigName, bool count)
202204
{
203205
var saCql = BuildCsqlFromParameters(serviceAreaNumbers, fromDate, toDate, dateColName);
204206

@@ -225,6 +227,12 @@ private string BuildQuery(decimal[] serviceAreaNumbers, DateTime fromDate, DateT
225227
pq.Remove(ExportQuery.ToDate);
226228
pq.Remove(ExportQuery.Format);
227229

230+
if (endpointConfigName == ExportQueryEndpointConfigName.WMS)
231+
{
232+
pq.Add(ExportQuery.Layers, pq[ExportQuery.TypeName]);
233+
pq.Remove(ExportQuery.TypeName);
234+
}
235+
228236
if (count)
229237
{
230238
pq.Add(ExportQuery.Count, "1");
@@ -253,7 +261,7 @@ private string BuildCsqlFromParameters(decimal[] serviceAreaNumbers, DateTime fr
253261

254262
csql.Append("SERVICE_AREA IN (");
255263

256-
foreach(var serviceAreaNumber in serviceAreaNumbers)
264+
foreach (var serviceAreaNumber in serviceAreaNumbers)
257265
{
258266
csql.Append($"{(int)serviceAreaNumber},");
259267
}

api/Hmcr.Api/Controllers/UsersController.cs

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
using Hmcr.Domain.Services;
44
using Hmcr.Model;
55
using Hmcr.Model.Dtos;
6+
using Hmcr.Model.Dtos.Keycloak;
67
using Hmcr.Model.Dtos.User;
78
using Hmcr.Model.Utils;
9+
using Microsoft.AspNetCore.Http;
810
using Microsoft.AspNetCore.Mvc;
911
using System;
10-
using System.Collections;
1112
using System.Collections.Generic;
1213
using System.Threading.Tasks;
1314

@@ -19,11 +20,13 @@ namespace Hmcr.Api.Controllers
1920
public class UsersController : HmcrControllerBase
2021
{
2122
private IUserService _userService;
23+
private IKeycloakService _keyCloakService;
2224
private HmcrCurrentUser _currentUser;
2325

24-
public UsersController(IUserService userService, HmcrCurrentUser currentUser)
26+
public UsersController(IUserService userService, IKeycloakService keyCloakService, HmcrCurrentUser currentUser)
2527
{
2628
_userService = userService;
29+
_keyCloakService = keyCloakService;
2730
_currentUser = currentUser;
2831
}
2932

@@ -83,6 +86,7 @@ public ActionResult<IEnumerable<UserTypeDto>> GetUserStatus()
8386
/// <param name="pageSize">Page size</param>
8487
/// <param name="pageNumber">Page number</param>
8588
/// <param name="orderBy">Order by column(s). Example: orderby=username</param>
89+
/// <param name="direction">Oder by direction. Example: asc, desc</param>
8690
/// <returns></returns>
8791
[HttpGet]
8892
[RequiresPermission(Permissions.UserRead)]
@@ -97,7 +101,7 @@ public async Task<ActionResult<PagedDto<UserSearchDto>>> GetUsersAsync(
97101
[RequiresPermission(Permissions.UserRead)]
98102
public async Task<ActionResult<UserDto>> GetUsersAsync(decimal id)
99103
{
100-
var user = await _userService.GetUserAsync(id);
104+
var user = await _userService.GetUserAsync(id);
101105

102106
if (user == null)
103107
return NotFound();
@@ -155,6 +159,7 @@ public async Task<ActionResult> UpdateUser(decimal id, UserUpdateDto user)
155159
return NoContent();
156160
}
157161

162+
158163
[HttpDelete("{id}")]
159164
[RequiresPermission(Permissions.UserWrite)]
160165
public async Task<ActionResult> DeleteUser(decimal id, UserDeleteDto user)
@@ -178,5 +183,52 @@ public async Task<ActionResult> DeleteUser(decimal id, UserDeleteDto user)
178183

179184
return NoContent();
180185
}
186+
187+
#region API Client
188+
[HttpGet("api-client", Name = "GetUserKeycloakClient")]
189+
public async Task<ActionResult<KeycloakClientDto>> GetUserKeycloakClient()
190+
{
191+
var client = await _keyCloakService.GetUserClientAsync();
192+
193+
if (client == null)
194+
{
195+
return NotFound();
196+
}
197+
198+
return Ok(client);
199+
}
200+
201+
[HttpPost("api-client")]
202+
public async Task<ActionResult<KeycloakClientDto>> CreateUserKeycloakClient()
203+
{
204+
var response = await _keyCloakService.CreateUserClientAsync();
205+
206+
if (response.Errors.Count > 0)
207+
{
208+
return ValidationUtils.GetValidationErrorResult(response.Errors, ControllerContext);
209+
}
210+
211+
return CreatedAtRoute("GetUserKeycloakClient", await _keyCloakService.GetUserClientAsync());
212+
}
213+
214+
[HttpPost("api-client/secret")]
215+
public async Task<ActionResult> RegenerateUserKeycloakClientSecret()
216+
{
217+
var response = await _keyCloakService.RegenerateUserClientSecretAsync();
218+
219+
if (response.NotFound)
220+
{
221+
return NotFound();
222+
}
223+
224+
if (!string.IsNullOrEmpty(response.Error))
225+
{
226+
return ValidationUtils.GetValidationErrorResult(ControllerContext,
227+
StatusCodes.Status500InternalServerError, "Unable to regenerate Keycloak client secret", response.Error);
228+
}
229+
230+
return CreatedAtRoute("GetUserKeycloakClient", await _keyCloakService.GetUserClientAsync());
231+
}
232+
#endregion
181233
}
182234
}

0 commit comments

Comments
 (0)