Skip to content

Commit c554bb1

Browse files
committed
Add platform configuration to restrict BackOffice menu option to internal users
1 parent ce2faf9 commit c554bb1

File tree

10 files changed

+181
-14
lines changed

10 files changed

+181
-14
lines changed

application/account-management/Core/Features/Users/Domain/User.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using PlatformPlatform.SharedKernel.Domain;
2+
using PlatformPlatform.SharedKernel.Platform;
23

34
namespace PlatformPlatform.AccountManagement.Features.Users.Domain;
45

@@ -37,6 +38,8 @@ public string Email
3738

3839
public string Locale { get; private set; }
3940

41+
public bool IsInternalUser => Email.EndsWith(Settings.Current.Identity.InternalEmailDomain, StringComparison.OrdinalIgnoreCase);
42+
4043
public TenantId TenantId { get; }
4144

4245
public static User Create(TenantId tenantId, string email, UserRole role, bool emailConfirmed, string? locale)

application/account-management/Core/Features/Users/Shared/UserInfoFactory.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ public async Task<UserInfo> CreateUserInfoAsync(User user, CancellationToken can
3232
Title = user.Title,
3333
AvatarUrl = user.Avatar.Url,
3434
TenantName = tenant?.Name,
35-
Locale = user.Locale
35+
Locale = user.Locale,
36+
IsInternalUser = user.IsInternalUser
3637
};
3738
}
3839
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using FluentAssertions;
2+
using PlatformPlatform.AccountManagement.Features.Users.Domain;
3+
using PlatformPlatform.SharedKernel.Domain;
4+
using PlatformPlatform.SharedKernel.Platform;
5+
using Xunit;
6+
7+
namespace PlatformPlatform.AccountManagement.Tests.Users.Domain;
8+
9+
public sealed class UserTests
10+
{
11+
private readonly TenantId _tenantId = TenantId.NewId();
12+
13+
[Fact]
14+
public void IsInternalUser_ShouldReturnTrueForInternalEmails()
15+
{
16+
// Arrange
17+
var internalEmails = new[]
18+
{
19+
$"user{Settings.Current.Identity.InternalEmailDomain}",
20+
$"admin{Settings.Current.Identity.InternalEmailDomain}",
21+
$"test.user{Settings.Current.Identity.InternalEmailDomain}",
22+
$"user+tag{Settings.Current.Identity.InternalEmailDomain}",
23+
$"USER{Settings.Current.Identity.InternalEmailDomain.ToUpperInvariant()}"
24+
};
25+
26+
foreach (var email in internalEmails)
27+
{
28+
// Arrange
29+
var user = User.Create(_tenantId, email, UserRole.Member, true, "en-US");
30+
31+
// Act
32+
var isInternal = user.IsInternalUser;
33+
34+
// Assert
35+
isInternal.Should().BeTrue($"Email {email} should be identified as internal");
36+
}
37+
}
38+
39+
[Fact]
40+
public void IsInternalUser_ShouldReturnFalseForExternalEmails()
41+
{
42+
// Arrange
43+
var externalEmails = new[]
44+
{
45+
"user@example.com",
46+
"user@company.net",
47+
$"{Settings.Current.Identity.InternalEmailDomain.Substring(1)}@example.com",
48+
$"user@subdomain.{Settings.Current.Identity.InternalEmailDomain.Substring(1)}"
49+
};
50+
51+
foreach (var email in externalEmails)
52+
{
53+
// Arrange
54+
var user = User.Create(_tenantId, email, UserRole.Member, true, "en-US");
55+
56+
// Act
57+
var isInternal = user.IsInternalUser;
58+
59+
// Assert
60+
isInternal.Should().BeFalse($"Email {email} should be identified as external");
61+
}
62+
}
63+
}

application/account-management/WebApp/federated-modules/sideMenu/NavigationMenuItems.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { t } from "@lingui/core/macro";
22
import { Trans } from "@lingui/react/macro";
3+
import { useUserInfo } from "@repo/infrastructure/auth/hooks";
34
import { FederatedMenuButton, SideMenuSeparator } from "@repo/ui/components/SideMenu";
45
import { BoxIcon, CircleUserIcon, HomeIcon, UsersIcon } from "lucide-react";
56
import type { FederatedSideMenuProps } from "./FederatedSideMenu";
@@ -8,6 +9,8 @@ import type { FederatedSideMenuProps } from "./FederatedSideMenu";
89
export function NavigationMenuItems({
910
currentSystem
1011
}: Readonly<{ currentSystem: FederatedSideMenuProps["currentSystem"] }>) {
12+
const userInfo = useUserInfo();
13+
1114
return (
1215
<>
1316
<FederatedMenuButton
@@ -34,16 +37,20 @@ export function NavigationMenuItems({
3437
isCurrentSystem={currentSystem === "account-management"}
3538
/>
3639

37-
<SideMenuSeparator>
38-
<Trans>Back Office</Trans>
39-
</SideMenuSeparator>
40+
{userInfo?.isInternalUser && (
41+
<>
42+
<SideMenuSeparator>
43+
<Trans>Back Office</Trans>
44+
</SideMenuSeparator>
4045

41-
<FederatedMenuButton
42-
icon={BoxIcon}
43-
label={t`Dashboard`}
44-
href="/back-office"
45-
isCurrentSystem={currentSystem === "back-office"}
46-
/>
46+
<FederatedMenuButton
47+
icon={BoxIcon}
48+
label={t`Dashboard`}
49+
href="/back-office"
50+
isCurrentSystem={currentSystem === "back-office"}
51+
/>
52+
</>
53+
)}
4754
</>
4855
);
4956
}

application/shared-kernel/SharedKernel/Authentication/UserInfo.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Security.Claims;
22
using PlatformPlatform.SharedKernel.Domain;
3+
using PlatformPlatform.SharedKernel.Platform;
34
using PlatformPlatform.SharedKernel.SinglePageApp;
45

56
namespace PlatformPlatform.SharedKernel.Authentication;
@@ -18,7 +19,8 @@ public class UserInfo
1819
public static readonly UserInfo System = new()
1920
{
2021
IsAuthenticated = false,
21-
Locale = DefaultLocale
22+
Locale = DefaultLocale,
23+
IsInternalUser = false
2224
};
2325

2426
public bool IsAuthenticated { get; init; }
@@ -43,32 +45,37 @@ public class UserInfo
4345

4446
public string? TenantName { get; init; }
4547

48+
public bool IsInternalUser { get; init; }
49+
4650
public static UserInfo Create(ClaimsPrincipal? user, string? browserLocale)
4751
{
4852
if (user?.Identity?.IsAuthenticated != true)
4953
{
5054
return new UserInfo
5155
{
5256
IsAuthenticated = user?.Identity?.IsAuthenticated ?? false,
53-
Locale = GetValidLocale(browserLocale)
57+
Locale = GetValidLocale(browserLocale),
58+
IsInternalUser = false
5459
};
5560
}
5661

5762
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
5863
var tenantId = user.FindFirstValue("tenant_id");
64+
var email = user.FindFirstValue(ClaimTypes.Email);
5965
return new UserInfo
6066
{
6167
IsAuthenticated = true,
6268
Id = userId == null ? null : new UserId(userId),
6369
TenantId = tenantId == null ? null : new TenantId(long.Parse(tenantId)),
6470
Role = user.FindFirstValue(ClaimTypes.Role),
65-
Email = user.FindFirstValue(ClaimTypes.Email),
71+
Email = email,
6672
FirstName = user.FindFirstValue(ClaimTypes.GivenName),
6773
LastName = user.FindFirstValue(ClaimTypes.Surname),
6874
Title = user.FindFirstValue("title"),
6975
AvatarUrl = user.FindFirstValue("avatar_url"),
7076
TenantName = user.FindFirstValue("tenant_name"),
71-
Locale = GetValidLocale(user.FindFirstValue("locale"))
77+
Locale = GetValidLocale(user.FindFirstValue("locale")),
78+
IsInternalUser = IsInternalUserEmail(email)
7279
};
7380
}
7481

@@ -91,4 +98,9 @@ private static string GetValidLocale(string? locale)
9198

9299
return foundLocale ?? DefaultLocale;
93100
}
101+
102+
private static bool IsInternalUserEmail(string? email)
103+
{
104+
return email is not null && email.EndsWith(Settings.Current.Identity.InternalEmailDomain, StringComparison.OrdinalIgnoreCase);
105+
}
94106
}

application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using PlatformPlatform.SharedKernel.Integrations.Email;
1616
using PlatformPlatform.SharedKernel.Persistence;
1717
using PlatformPlatform.SharedKernel.PipelineBehaviors;
18+
using PlatformPlatform.SharedKernel.Platform;
1819
using PlatformPlatform.SharedKernel.Telemetry;
1920

2021
namespace PlatformPlatform.SharedKernel.Configuration;
@@ -39,6 +40,7 @@ public static IServiceCollection AddSharedServices<T>(this IServiceCollection se
3940
return services
4041
.AddServiceDiscovery()
4142
.AddSingleton(GetTokenSigningService())
43+
.AddSingleton(Settings.Current)
4244
.AddAuthentication()
4345
.AddDefaultJsonSerializerOptions()
4446
.AddPersistenceHelpers<T>()
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System.Text.Json;
2+
3+
namespace PlatformPlatform.SharedKernel.Platform;
4+
5+
public sealed class Settings
6+
{
7+
private static readonly Lazy<Settings> Instance = new(LoadFromEmbeddedResource);
8+
9+
public static Settings Current => Instance.Value;
10+
11+
public required IdentityConfig Identity { get; init; }
12+
13+
public required BrandingConfig Branding { get; init; }
14+
15+
private static Settings LoadFromEmbeddedResource()
16+
{
17+
var assembly = Assembly.GetExecutingAssembly();
18+
var resourceName = "PlatformPlatform.SharedKernel.Platform.platform-settings.jsonc";
19+
20+
using var stream = assembly.GetManifestResourceStream(resourceName)
21+
?? throw new InvalidOperationException($"Could not find embedded resource: {resourceName}");
22+
23+
var options = new JsonSerializerOptions
24+
{
25+
PropertyNameCaseInsensitive = true,
26+
ReadCommentHandling = JsonCommentHandling.Skip
27+
};
28+
29+
return JsonSerializer.Deserialize<Settings>(stream, options)
30+
?? throw new InvalidOperationException("Failed to deserialize platform settings");
31+
}
32+
33+
public sealed class IdentityConfig
34+
{
35+
public required string InternalEmailDomain { get; init; }
36+
}
37+
38+
public sealed class BrandingConfig
39+
{
40+
public required string ProductName { get; init; }
41+
42+
public required string SupportEmail { get; init; }
43+
}
44+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
// Platform-wide configuration settings
3+
// This file is prepared for sharing between backend (.NET), frontend (TypeScript), and tests
4+
//
5+
// IMPORTANT: This configuration is embedded in the backend and injected at build time in the frontend
6+
// Not all values are currently used - some are placeholders for future functionality
7+
//
8+
// Security Note: Only include non-sensitive configuration here
9+
// Sensitive values (like API keys) should be stored in environment variables or key vaults
10+
11+
"identity": {
12+
// Email domain suffix that identifies internal users
13+
// Users with this domain get access to BackOffice and other internal features
14+
// Currently used by backend only - frontend relies on isInternalUser flag from backend
15+
"internalEmailDomain": "@platformplatform.net"
16+
},
17+
18+
"branding": {
19+
// Product/platform name used throughout the application
20+
// Placeholder for future use - currently hardcoded in various places
21+
"productName": "PlatformPlatform",
22+
23+
// Support email address for user inquiries
24+
// Placeholder for future use - not currently referenced in the codebase
25+
"supportEmail": "support@platformplatform.net"
26+
}
27+
}

application/shared-kernel/SharedKernel/SharedKernel.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,8 @@
5757
<PackageReference Include="Scrutor" />
5858
</ItemGroup>
5959

60+
<ItemGroup>
61+
<EmbeddedResource Include="Platform\platform-settings.jsonc" />
62+
</ItemGroup>
63+
6064
</Project>

application/shared-webapp/build/environment.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ export declare global {
8484
* Tenant name
8585
**/
8686
tenantName?: string;
87+
/**
88+
* Is internal user (has access to BackOffice)
89+
**/
90+
isInternalUser?: boolean;
8791
}
8892

8993
/**

0 commit comments

Comments
 (0)