Skip to content

Commit 01b6320

Browse files
authored
Add upload logo functionality and display account name and logo in navigation (#782)
### Summary & Motivation Add support for uploading account logos and show the account names in the side menu and invitation emails. While the account name could previously be changed, it was not used anywhere. It is now displayed in the side menu with the logo to clearly indicate which account the user is currently logged into. - Implement backend storage for account logos using Azure Blob Storage using shared access signatures - Create frontend UI components for uploading, displaying, and removing account logos in the account settings - Display logo and account name in the side menu - Add logic to prevent user invitations when an account lacks a name - Rename blob storage environment variable in AppGateway Bicep infrastructure from `AVATARS_STORAGE_URL` to `ACCOUNT_MANAGEMENT_STORAGE_URL` for unified storage connection. This allows using the same connection string for both logos and avatars stored in the Account Management Blob Storage account. ### Downstream projects **Important: Infrastructure changes require careful deployment to avoid downtime** The environment variable `AVATARS_STORAGE_URL` has been renamed to `ACCOUNT_MANAGEMENT_STORAGE_URL` in the AppGateway infrastructure. Deployment steps to avoid downtime: 1. Manually add the `ACCOUNT_MANAGEMENT_STORAGE_URL` environment variable to AppGateway with the same value as the existing `AVATARS_STORAGE_URL` 2. Deploy the new AppGateway: `.github/workflows/app-gateway.yml` 3. Deploy Account Management: `.github/workflows/account-management.yml` 4. After verifying the services are running correctly, deploy the infrastructure which will remove the old `AVATARS_STORAGE_URL` environment variable using this: `.github/workflows/cloud-infrastructure.yml` Note: This deployment sequence has not been tested. Please verify in your staging environment first. Reminder: Deploying infrastructure in parallel with self-contained systems may revert the active Docker version to the previous one. This occurs because infrastructure deployment begins by retrieving the active version and adding it to the Bicep deployment. If a new version is deployed during this process, Bicep might revert it. ### Checklist - [x] I have added tests, or done manual regression tests - [x] I have updated the documentation, if necessary
2 parents 892459e + f799713 commit 01b6320

Some content is hidden

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

46 files changed

+1100
-81
lines changed

application/AppGateway/Filters/ClusterDestinationConfigFilter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public ValueTask<ClusterConfig> ConfigureClusterAsync(ClusterConfig cluster, Can
1010
{
1111
"account-management-api" => ReplaceDestinationAddress(cluster, "ACCOUNT_MANAGEMENT_API_URL"),
1212
"account-management-static" => ReplaceDestinationAddress(cluster, "ACCOUNT_MANAGEMENT_API_URL"),
13-
"avatars-storage" => ReplaceDestinationAddress(cluster, "AVATARS_STORAGE_URL"),
13+
"account-management-storage" => ReplaceDestinationAddress(cluster, "ACCOUNT_MANAGEMENT_STORAGE_URL"),
1414
"back-office-api" => ReplaceDestinationAddress(cluster, "BACK_OFFICE_API_URL"),
1515
"back-office-static" => ReplaceDestinationAddress(cluster, "BACK_OFFICE_API_URL"),
1616
_ => throw new InvalidOperationException($"Unknown Cluster ID {cluster.ClusterId}.")

application/AppGateway/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
);
3939
}
4040

41-
builder.AddNamedBlobStorages(("avatars-storage", "AVATARS_STORAGE_URL"));
41+
builder.AddNamedBlobStorages(("account-management-storage", "ACCOUNT_MANAGEMENT_STORAGE_URL"));
4242

4343
builder.WebHost.UseKestrel(option => option.AddServerHeader = false);
4444

application/AppGateway/Transformations/ManagedIdentityTransform.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ public class ManagedIdentityTransform(TokenCredential credential)
88
{
99
protected override string? GetValue(RequestTransformContext context)
1010
{
11-
if (!context.HttpContext.Request.Path.StartsWithSegments("/avatars", StringComparison.OrdinalIgnoreCase))
11+
if (!context.HttpContext.Request.Path.StartsWithSegments("/avatars", StringComparison.OrdinalIgnoreCase) &&
12+
!context.HttpContext.Request.Path.StartsWithSegments("/logos", StringComparison.OrdinalIgnoreCase))
1213
{
1314
return null;
1415
}
@@ -23,6 +24,9 @@ public class ApiVersionHeaderTransform() : RequestHeaderTransform("x-ms-version"
2324
{
2425
protected override string? GetValue(RequestTransformContext context)
2526
{
26-
return !context.HttpContext.Request.Path.StartsWithSegments("/avatars") ? null : "2023-11-03";
27+
return !context.HttpContext.Request.Path.StartsWithSegments("/avatars") &&
28+
!context.HttpContext.Request.Path.StartsWithSegments("/logos")
29+
? null
30+
: "2023-11-03";
2731
}
2832
}

application/AppGateway/Transformations/SharedAccessSignatureRequestTransform.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,26 @@
33

44
namespace PlatformPlatform.AppGateway.Transformations;
55

6-
public class SharedAccessSignatureRequestTransform([FromKeyedServices("avatars-storage")] BlobStorageClient blobStorageClient)
6+
public class SharedAccessSignatureRequestTransform([FromKeyedServices("account-management-storage")] BlobStorageClient accountManagementBlobStorageClient)
77
: RequestTransform
88
{
99
public override ValueTask ApplyAsync(RequestTransformContext context)
1010
{
11-
if (!context.Path.StartsWithSegments("/avatars")) return ValueTask.CompletedTask;
11+
string containerName;
12+
if (context.Path.StartsWithSegments("/avatars"))
13+
{
14+
containerName = "avatars";
15+
}
16+
else if (context.Path.StartsWithSegments("/logos"))
17+
{
18+
containerName = "logos";
19+
}
20+
else
21+
{
22+
return ValueTask.CompletedTask;
23+
}
1224

13-
var sharedAccessSignature = blobStorageClient.GetSharedAccessSignature("avatars", TimeSpan.FromMinutes(10));
25+
var sharedAccessSignature = accountManagementBlobStorageClient.GetSharedAccessSignature(containerName, TimeSpan.FromMinutes(10));
1426
context.HttpContext.Request.QueryString = new QueryString(sharedAccessSignature);
1527

1628
return ValueTask.CompletedTask;

application/AppGateway/appsettings.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
}
6969
},
7070
"avatars": {
71-
"ClusterId": "avatars-storage",
71+
"ClusterId": "account-management-storage",
7272
"Match": {
7373
"Path": "/avatars/{**catch-all}"
7474
},
@@ -79,6 +79,18 @@
7979
}
8080
]
8181
},
82+
"logos": {
83+
"ClusterId": "account-management-storage",
84+
"Match": {
85+
"Path": "/logos/{**catch-all}"
86+
},
87+
"Transforms": [
88+
{
89+
"ResponseHeader": "Cache-Control",
90+
"Set": "public, max-age=2592000, immutable"
91+
}
92+
]
93+
},
8294
"account-management-api": {
8395
"ClusterId": "account-management-api",
8496
"Match": {
@@ -205,7 +217,7 @@
205217
}
206218
}
207219
},
208-
"avatars-storage": {
220+
"account-management-storage": {
209221
"Destinations": {
210222
"destination": {
211223
"Address": "http://127.0.0.1:10000/devstoreaccount1"

application/AppHost/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
.WithUrlForEndpoint("http", u => u.DisplayText = "Read mail here");
4646

4747
CreateBlobContainer("avatars");
48+
CreateBlobContainer("logos");
4849

4950
var frontendBuild = builder
5051
.AddNpmApp("frontend-build", "../")

application/account-management/Api/Endpoints/TenantEndpoints.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ public void MapEndpoints(IEndpointRouteBuilder routes)
2222
=> (await mediator.Send(command)).AddRefreshAuthenticationTokens()
2323
);
2424

25+
group.MapPost("/current/update-logo", async Task<ApiResult> (IFormFile file, IMediator mediator)
26+
=> await mediator.Send(new UpdateTenantLogoCommand(file.OpenReadStream(), file.ContentType))
27+
).DisableAntiforgery();
28+
29+
group.MapDelete("/current/remove-logo", async Task<ApiResult> (IMediator mediator)
30+
=> await mediator.Send(new RemoveTenantLogoCommand())
31+
);
32+
2533
routes.MapDelete("/internal-api/account-management/tenants/{id}", async Task<ApiResult> (TenantId id, IMediator mediator)
2634
=> await mediator.Send(new DeleteTenantCommand(id))
2735
);

application/account-management/Core/Configuration.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.Extensions.DependencyInjection;
22
using Microsoft.Extensions.Hosting;
33
using PlatformPlatform.AccountManagement.Database;
4+
using PlatformPlatform.AccountManagement.Features.Tenants;
45
using PlatformPlatform.AccountManagement.Features.Users.Shared;
56
using PlatformPlatform.AccountManagement.Integrations.Gravatar;
67
using PlatformPlatform.SharedKernel.Configuration;
@@ -16,7 +17,7 @@ public static IHostApplicationBuilder AddAccountManagementInfrastructure(this IH
1617
// Infrastructure is configured separately from other Infrastructure services to allow mocking in tests
1718
return builder
1819
.AddSharedInfrastructure<AccountManagementDbContext>("account-management-database")
19-
.AddNamedBlobStorages(("avatars-storage", "BLOB_STORAGE_URL"));
20+
.AddNamedBlobStorages(("account-management-storage", "BLOB_STORAGE_URL"));
2021
}
2122

2223
public static IServiceCollection AddAccountManagementServices(this IServiceCollection services)
@@ -28,6 +29,8 @@ public static IServiceCollection AddAccountManagementServices(this IServiceColle
2829
}
2930
);
3031

32+
TenantMapsterConfig.Configure();
33+
3134
return services
3235
.AddSharedServices<AccountManagementDbContext>(Assembly)
3336
.AddScoped<AvatarUpdater>()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Microsoft.EntityFrameworkCore.Infrastructure;
2+
using Microsoft.EntityFrameworkCore.Migrations;
3+
4+
namespace PlatformPlatform.AccountManagement.Database.Migrations;
5+
6+
[DbContext(typeof(AccountManagementDbContext))]
7+
[Migration("20250804001944_AddTenantLogo")]
8+
public sealed class AddTenantLogo : Migration
9+
{
10+
protected override void Up(MigrationBuilder migrationBuilder)
11+
{
12+
migrationBuilder.AddColumn<string>(
13+
name: "Logo",
14+
table: "Tenants",
15+
type: "varchar(150)",
16+
nullable: false,
17+
defaultValue: "{}");
18+
}
19+
}

application/account-management/Core/Features/TelemetryEvents.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ public sealed class TenantCreated(TenantId tenantId, TenantState state)
5151
public sealed class TenantDeleted(TenantId tenantId, TenantState tenantState, int usersDeleted)
5252
: TelemetryEvent(("tenant_id", tenantId), ("tenant_state", tenantState), ("users_deleted", usersDeleted));
5353

54+
public sealed class TenantLogoRemoved
55+
: TelemetryEvent;
56+
57+
public sealed class TenantLogoUpdated(string contentType, long size)
58+
: TelemetryEvent(("content_type", contentType), ("size", size));
59+
5460
public sealed class TenantUpdated
5561
: TelemetryEvent;
5662

0 commit comments

Comments
 (0)