Skip to content

Commit

Permalink
Merge pull request #33 from OS2Valghalla/dev
Browse files Browse the repository at this point in the history
Latest development
  • Loading branch information
ramogens-OS2 authored Apr 4, 2024
2 parents a700924 + 9183312 commit be115a3
Show file tree
Hide file tree
Showing 158 changed files with 12,977 additions and 1,110 deletions.
101 changes: 101 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,104 @@ Completely re-written version of OS2valghalla.
Documentation is available at: [https://os2valghalla.readthedocs.io/en/latest/](https://os2valghalla.readthedocs.io/en/latest/) or [OS2valghalla 3 documentation](https://github.com/OS2Valghalla/OS2valghalla-3-documentation) if you prefer to stay in GitHub.

See [OS2valghalla 3 documentation for contributing](https://github.com/OS2Valghalla/OS2valghalla-3-documentation/blob/main/docs/source/contribution/index.rst)

## Development guideline
### Installation
Recommended IDEs:
- Frontend development: [VSCode](https://code.visualstudio.com/)
- Backend development: [Visual Studio](https://visualstudio.microsoft.com/)

Other installations:
- [RabbitMQ](https://www.rabbitmq.com/)
- [PostgreSQL](https://www.postgresql.org/)

You need to install all NPM packages in Valghalla.Internal.Web folder and Valghalla.External.Web folder (use any package manager you prefer, for example using NPM command line: `npm install`). Nuget packages can be restored directly in Visual Studio.

### Application settings
There are 3 applications we need to configure app settings for development: Valghalla.Internal.API, Valghalla.External.API and Valghalla.Worker. Most of these files are similar to each other as they're configured for local hosting domain.
The main application settings file is in Environment folder at the top of source control, here you should clone `secrets.json` file to `secrets.development.json`.
The application authentication works based on SAML so you need to configure custom domain to your localhost.
For example in Windows OS, add custom domain to your host via `C:\Windows\System32\drivers\etc\hosts`.
#### secrets.development.json
- Queue: please configure like the one you setup in RabbitMQ
- Tenants: provide at least one local tenant for development, for example
```json
"Tenants": [
{
"Name": "Municipality (Dev)",
"ConnectionString": "Host=localhost;port=5432;Database=DevDatabase;Username=dev;Password=dev",
"InternalDomain": "localhost",
"ExternalDomain": "localhost"
},
]
```
- CPRService, Mail, SMS, DigitalPost: please contact providers to get the information and put in the correct places.
#### appsettings.development.json (Valghalla.Internal.API)
Please configure similar to these settings
```json
{
"Urls": "https://<custom domain>:20002", // Valghalla.Internal.API port
"Secrets": {
"Path": "../../../../Environment/secrets.development.json" // Path to secrets.development.json file
},
"Authentication": {
"IdPMetadataFile": "<path to metadata file>" // Path to metadata xml file
},
"AllowedOrigins": [
"https://<custom domain>:4200", // Valghalla.Internal.Web port
"https://<custom domain>:20002" // Valghalla.Internal.API port
],
"AngularDevServer": "https://<custom domain>:4200", // Valghalla.Internal.Web port
"HealthChecks-UI": {
"HealthChecks": [
{
"Name": "Valghalla",
"Uri": "https://localhost:7174/health"
}
]
}
}
```
#### appsettings.development.json (Valghalla.External.API)
Please configure similar to these settings
```json
{
"Urls": "https://<custom domain>:20001", // Valghalla.External.API port
"Secrets": {
"Path": "../../../../Environment/secrets.development.json" // Path to secrets.development.json file
},
"Authentication": {
"IdPMetadataFile": "<path to metadata file>" // Path to metadata xml file
},
"AllowedOrigins": [
"https://<custom domain>:4600", // Valghalla.External.Web port
"https://<custom domain>:20001", // Valghalla.External.API port
"https://test-devtest4-nemlog-in.dk" // Test nem login domain
],
"AngularDevServer": "https://<custom domain>:4600" // Valghalla.External.Web port
}
```
#### appsettings.development.json (Valghalla.Worker)
Please configure similar to these settings
```json
{
"Secrets": {
"Path": "../../../../Environment/secrets.development.json" // Path to secrets.development.json file
},
"Job": {
"ConnectionString": "Host=localhost;port=5432;Database=DevJobDatabase;Username=dev;Password=dev" // Database for worker job
}
}
```

### Start development
Make sure PostgreSQL server and RabbitMQ server are up and running then start the following applications in https:
- Valghalla.Internal.API
- Valghalla.External.API
- Valghalla.Worker

Then start following applications via command line `npm run start` (make sure to run the command in the correct folder):
- Valghalla.Internal.Web
- Valghalla.External.Web

You should see the addresses to access applications from console.
2 changes: 1 addition & 1 deletion Valghalla.Application/AuditLog/ParticipantAuditLog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public ParticipantAuditLog(Guid participantId, string firstName, string lastName
{
Pk2value = participantId;
Col2value = firstName + " " + lastName;
Col3value = $"{PadTimeValue(birthDate.Day)}/{PadTimeValue(birthDate.Month)}/{PadTimeValue(birthDate.Year)}";
Col3value = $"{PadTimeValue(birthDate.ToLocalTime().Day)}/{PadTimeValue(birthDate.ToLocalTime().Month)}/{PadTimeValue(birthDate.ToLocalTime().Year)}";
}

private static string PadTimeValue(int value) => value.ToString().PadLeft(2, '0');
Expand Down
26 changes: 26 additions & 0 deletions Valghalla.Application/Auth/AuthenticationUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;

namespace Valghalla.Application.Auth
{
public static class AuthenticationUtilities
{
public static bool IsAnonymousEndpoint(HttpContext context) => context.GetEndpoint()?.Metadata?.GetMetadata<IAllowAnonymous>() != null;

public static bool IsApiEndpoint(HttpContext context) => context.Request.Path.HasValue && context.Request.Path.Value.Contains("/api/");

public static async Task SetUnauthorizedResponseAsync(HttpContext context, CancellationToken cancellationToken)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync(string.Empty, cancellationToken);
}

public static async Task SetTokenExpiredResponseAsync(HttpContext context, CancellationToken cancellationToken)
{
context.Response.ContentType = "text/plain";
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync("__TOKEN_EXPIRED__", cancellationToken);
}
}
}
24 changes: 24 additions & 0 deletions Valghalla.Application/Auth/ClaimsPrincipalExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Security.Claims;

namespace Valghalla.Application.Auth
{
public static class ClaimsPrincipalExtension
{
public const string Name = "valghalla_Name";
public const string Cpr = "valghalla_Cpr";
public const string Cvr = "valghalla_Cvr";
public const string Serial = "valghalla_Serial";

public const string Saml2NameIdFormat = "http://schemas.itfoxtec.com/ws/2014/02/identity/claims/saml2nameidformat";
public const string Saml2NameId = "http://schemas.itfoxtec.com/ws/2014/02/identity/claims/saml2nameid";
public const string Saml2SessionIndex = "http://schemas.itfoxtec.com/ws/2014/02/identity/claims/saml2sessionindex";

public static string? GetName(this ClaimsPrincipal principal) => principal.FindFirstValue(Name);
public static string? GetCpr(this ClaimsPrincipal principal) => principal.FindFirstValue(Cpr);
public static string? GetCvr(this ClaimsPrincipal principal) => principal.FindFirstValue(Cvr);
public static string? GetSerial(this ClaimsPrincipal principal) => principal.FindFirstValue(Serial);
public static string? GetSaml2NameIdFormat(this ClaimsPrincipal principal) => principal.FindFirstValue(Saml2NameIdFormat);
public static string? GetSaml2NameId(this ClaimsPrincipal principal) => principal.FindFirstValue(Saml2NameId);
public static string? GetSaml2SessionIndex(this ClaimsPrincipal principal) => principal.FindFirstValue(Saml2SessionIndex);
}
}
8 changes: 8 additions & 0 deletions Valghalla.Application/Auth/IUserTokenConfigurator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Valghalla.Application.Auth
{
public interface IUserTokenConfigurator
{
string CookieName { get; }
bool Renewable { get; }
}
}
11 changes: 11 additions & 0 deletions Valghalla.Application/Auth/IUserTokenManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Security.Claims;

namespace Valghalla.Application.Auth
{
public interface IUserTokenManager
{
void ExpireUserToken();
Task<UserToken?> EnsureUserTokenAsync(CancellationToken cancellationToken);
Task<UserToken?> EnsureUserTokenAsync(ClaimsPrincipal principal, CancellationToken cancellationToken);
}
}
9 changes: 9 additions & 0 deletions Valghalla.Application/Auth/IUserTokenRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Valghalla.Application.Auth
{
public interface IUserTokenRepository
{
Task<IEnumerable<UserToken>> GetUserTokensAsync(Guid identifier, CancellationToken cancellationToken);
Task AddUserTokenAsync(UserToken token, CancellationToken cancellationToken);
Task RemoveExpiredUserTokensAsync(CancellationToken cancellationToken);
}
}
106 changes: 106 additions & 0 deletions Valghalla.Application/Auth/UserToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

namespace Valghalla.Application.Auth
{
public class UserToken
{
private static readonly int LIFETIME_PERIOD_MINUTES = 60;
private static readonly int RENEWABLE_AFTER_MINUTES = 30;

public class TokenKey
{
public Guid Identifier { get; init; }
public string Code { get; init; } = null!;
}

public class TokenValue
{
public string? Name { get; init; }
public string? Cvr { get; init; }
public string? Cpr { get; init; }
public string? Serial { get; init; }
public string? Saml2NameIdFormat { get; init; }
public string? Saml2NameId { get; init; }
public string? Saml2SessionIndex { get; init; }
}

public TokenKey Key { get; init; } = new();
public TokenValue Value { get; init; } = new();
public DateTime CreatedAt { get; init; }
public DateTime ExpiredAt { get; init; }
public DateTime RefreshedAfter => CreatedAt.AddMinutes(RENEWABLE_AFTER_MINUTES);

public bool Valid => DateTime.UtcNow < ExpiredAt;
public bool Renewable => Valid && DateTime.UtcNow > RefreshedAfter;

public ClaimsPrincipal ToClaimsPrincipal(bool includeSessionIndex = true)
{
var claims = new List<Claim> {
new(ClaimTypes.Name, Value.Saml2NameId ?? Key.Identifier.ToString()),
new(ClaimTypes.NameIdentifier, Value.Saml2NameId ?? Key.Identifier.ToString()),
new(ClaimsPrincipalExtension.Name, Value.Name ?? string.Empty),
new(ClaimsPrincipalExtension.Cvr, Value.Cvr ?? string.Empty),
new(ClaimsPrincipalExtension.Cpr, Value.Cpr ?? string.Empty),
new(ClaimsPrincipalExtension.Serial, Value.Serial ?? string.Empty),
new(ClaimsPrincipalExtension.Saml2NameIdFormat, Value.Saml2NameIdFormat ?? string.Empty),
new(ClaimsPrincipalExtension.Saml2NameId, Value.Saml2NameId ?? string.Empty)
};

if (includeSessionIndex)
{
claims.Add(new(ClaimsPrincipalExtension.Saml2SessionIndex, Value.Saml2SessionIndex ?? string.Empty));
}

var identity = new ClaimsIdentity(claims, Constants.Authentication.Scheme);
return new(identity);
}

public static UserToken CreateToken(ClaimsPrincipal principal) => new()
{
Key = new TokenKey()
{
Identifier = Guid.NewGuid(),
Code = GenerateCode()
},
Value = new TokenValue()
{
Name = principal.GetName(),
Cvr = principal.GetCvr(),
Cpr = principal.GetCpr(),
Serial = principal.GetSerial(),
Saml2NameIdFormat = principal.GetSaml2NameIdFormat(),
Saml2NameId = principal.GetSaml2NameId(),
Saml2SessionIndex = principal.GetSaml2SessionIndex(),
},
CreatedAt = DateTime.UtcNow,
ExpiredAt = DateTime.UtcNow.AddMinutes(LIFETIME_PERIOD_MINUTES)
};

public static string Encode(TokenKey value)
{
return Base64UrlEncoder.Encode(JsonSerializer.Serialize(value));
}

public static TokenKey? Decode(string value)
{
return JsonSerializer.Deserialize<TokenKey>(Base64UrlEncoder.Decode(value));
}

private static string GenerateCode()
{
var salt = Guid.NewGuid().ToString();
var hashObject = new HMACSHA512(Encoding.UTF8.GetBytes(Constants.Authentication.Cookie));
var signature = hashObject.ComputeHash(Encoding.UTF8.GetBytes(salt));
var encodedSignature = Convert.ToBase64String(signature)
.Replace("+", string.Empty)
.Replace("=", string.Empty)
.Replace("/", string.Empty);

return encodedSignature;
}
}
}
6 changes: 5 additions & 1 deletion Valghalla.Application/Cache/ITenantMemoryCache.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
namespace Valghalla.Application.Cache
using Microsoft.Extensions.Caching.Memory;

namespace Valghalla.Application.Cache
{
public interface ITenantMemoryCache
{
TItem? GetOrCreate<TItem>(string key, Func<TItem> factory);
TItem? GetOrCreate<TItem>(string key, Func<ICacheEntry, TItem> factory);
Task<TItem?> GetOrCreateAsync<TItem>(string key, Func<Task<TItem>> factory);
Task<TItem?> GetOrCreateAsync<TItem>(string key, Func<ICacheEntry, Task<TItem>> factory);
void Remove(string key);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ public sealed record CommunicationTaskTypeInfo
public string Description { get; init; } = null!;
public int? Payment { get; init; }
public TimeSpan StartTime { get; init; }
public TimeSpan EndTime { get; init; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public interface ICommunicationHelper
string ReplaceTokens(string template, CommunicationRelatedInfo info);
Task<bool> ValidateTaskInvitationAsync(Guid participantId, Guid taskAssignmentId, CancellationToken cancellationToken);
Task<bool> ValidateRemovedFromTaskAsync(Guid participantId, Guid taskAssignmentId, CancellationToken cancellationToken);
Task<bool> ValidateRemovedFromTaskByValidationAsync(Guid participantId, Guid taskAssignmentId, CancellationToken cancellationToken);
Task<bool> ValidateTaskInvitationReminderAsync(Guid participantId, Guid taskAssignmentId, CancellationToken cancellationToken);
Task<bool> ValidateTaskReminderAsync(Guid participantId, Guid taskAssignmentId, CancellationToken cancellationToken);
Task<bool> ValidateTaskRegistrationAsync(Guid participantId, Guid taskAssignmentId, CancellationToken cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public interface ICommunicationQueryRepository
Task<CommunicationRelatedInfo?> GetRejectedTaskInfoAsync(Guid rejectedTaskId, CancellationToken cancellationToken);
Task<CommunicationTemplate?> GetTaskInvitationCommunicationTemplateAsync(Guid taskAssignmentId, CancellationToken cancellationToken);
Task<CommunicationTemplate?> GetRemovedFromTaskCommunicationTemplateAsync(Guid taskAssignmentId, CancellationToken cancellationToken);
Task<CommunicationTemplate?> GetRemovedFromTaskByValidationCommunicationTemplateAsync(Guid taskAssignmentId, CancellationToken cancellationToken);
Task<CommunicationTemplate?> GetTaskRegistrationCommunicationTemplateAsync(Guid taskAssignmentId, CancellationToken cancellationToken);
Task<CommunicationTemplate?> GetTaskCancellationCommunicationTemplateAsync(Guid taskAssignmentId, CancellationToken cancellationToken);
Task<CommunicationTemplate?> GetTaskInvitationReminderCommunicationTemplateAsync(Guid taskAssignmentId, CancellationToken cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
public interface ICommunicationService
{
Task SendTaskInvitationAsync(Guid participantId, Guid taskAssignmentId, CancellationToken cancellationToken);
Task SendRemovedFromTaskByValidationAsync(Guid participantId, Guid taskAssignmentId, CancellationToken cancellationToken);
Task SendRemovedFromTaskAsync(Guid participantId, Guid taskAssignmentId, CancellationToken cancellationToken);
Task SendTaskRegistrationAsync(Guid participantId, Guid taskAssignmentId, CancellationToken cancellationToken);
Task SendTaskCancellationAsync(Guid participantId, Guid taskAssignmentId, CancellationToken cancellationToken);
Expand Down
2 changes: 2 additions & 0 deletions Valghalla.Application/Configuration/AppConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public class AppConfiguration : IConfiguration
public string SmsSender { get; init; } = null!;
public string MailSender { get; init; } = null!;
public string MailAddress { get; init; } = null!;
public string ReplyToMailSender { get; init; } = null!;
public string ReplyToMailAddress { get; init; } = null!;
public string TaskReminderDay { get; init; } = null!;
}
}
4 changes: 2 additions & 2 deletions Valghalla.Application/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ public static class AuditLog

public static class Authentication
{
public const string LoginPath = "/api/auth/login";
public const string LogoutPath = "/api/auth/logout";
public const string Scheme = "ValghallaToken";
public const string Cookie = "ValghallaTokenKey";
}

public static class DefaultCommunicationTemplates
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Valghalla.Application.Queue.Messages
{
public sealed record RemovedFromTaskByValidationJobMessage
{
public Guid ParticipantId { get; init; }
public Guid TaskAssignmentId { get; init; }
}
}
9 changes: 9 additions & 0 deletions Valghalla.Application/Saml/ISaml2AuthPostProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Security.Claims;

namespace Valghalla.Application.Saml
{
public interface ISaml2AuthPostProcessor
{
Task<ClaimsPrincipal> HandleAsync(ClaimsPrincipal claimsPrincipal, CancellationToken cancellationToken);
}
}
Loading

0 comments on commit be115a3

Please sign in to comment.