Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customizable Branding #112

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/Pixel.Identity.Core/Controllers/BrandingController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Pixel.Identity.Shared.Branding;

namespace Pixel.Identity.Core.Controllers;

[ApiController]
[AllowAnonymous]
[Route("api/[controller]")]
[ApiExplorerSettings(IgnoreApi = true)]
public class BrandingController(IBrandingService brandService) : Controller
{
private readonly IBrandingService _brandService = brandService;

[HttpGet]
public async Task<IActionResult> GetAsync()
{
var brand = await _brandService.GetBrandAsync();
return Ok(brand);
}
}
22 changes: 22 additions & 0 deletions src/Pixel.Identity.Core/Services/AppSettingsBrandService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Pixel.Identity.Shared.Branding;

namespace Pixel.Identity.UI.Client.Services;

public class AppSettingsBrandService(IConfiguration configuration) : IBrandingService
{
private readonly IConfiguration _configuration = configuration;

private Brand _brand;

public async Task<Brand> GetBrandAsync() => _brand ??= await BuildAsync();

private Task<Brand> BuildAsync()
{
var name = _configuration["Brand:Name"] ?? BrandingProperties.Name;
var shortName = _configuration["Brand:ShortName"] ?? BrandingProperties.ShortName;
var logoUriDark = _configuration["Brand:LogoUriDark"] ?? BrandingProperties.LogoUriDark;
var logoUriLight = _configuration["Brand:LogoUriLight"] ?? BrandingProperties.LogoUriLight;

return Task.FromResult(new Brand(name, shortName, logoUriDark, logoUriLight));
}
}
54 changes: 32 additions & 22 deletions src/Pixel.Identity.Provider/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using Pixel.Identity.Provider.Components;
using Pixel.Identity.Provider.Extensions;
using Pixel.Identity.Shared;
using Pixel.Identity.Shared.Branding;
using Pixel.Identity.UI.Client.Services;
using Quartz;
using Serilog;
using System.IO;
Expand Down Expand Up @@ -50,12 +52,12 @@ public void ConfigureServices(IServiceCollection services)
});

//Add plugin assembly type to application part so that controllers in this assembly can be discovered by asp.net
services.AddControllersWithViews();
services.AddControllersWithViews();
services.AddRazorPages(options =>
{
{
//Allow unauthorized users to access registration page when allow user registration setting is true so that users can
//register for a new account on their own.
if(bool.TryParse(Configuration["AllowUserRegistration"] ?? "true", out bool allowUserRegistration) && allowUserRegistration)
if (bool.TryParse(Configuration["AllowUserRegistration"] ?? "true", out bool allowUserRegistration) && allowUserRegistration)
{
options.Conventions.AllowAnonymousToAreaPage("Identity", "/Account/Register");
}
Expand All @@ -69,7 +71,7 @@ public void ConfigureServices(IServiceCollection services)
{
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
});

services.AddMudServices(config =>
{
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.TopRight;
Expand All @@ -83,7 +85,7 @@ public void ConfigureServices(IServiceCollection services)

ConfigureCors(services);

services.AddPlugin<IServicePlugin>(pluginsOptions["EmailSender"].Single(), (p, s) =>
services.AddPlugin<IServicePlugin>(pluginsOptions["EmailSender"].Single(), (p, s) =>
{
p.ConfigureService(s, this.Configuration);
});
Expand All @@ -92,19 +94,21 @@ public void ConfigureServices(IServiceCollection services)
foreach (var externalProvider in pluginsOptions["OAuthProvider"])
{
services.AddPlugin<IExternalAuthProvider>(externalProvider, (p, s) =>
{
p.AddProvider(this.Configuration, authenticationBuilder);
});
}
{
p.AddProvider(this.Configuration, authenticationBuilder);
});
}

services.AddPlugin<IDataStoreConfigurator>(pluginsOptions["DbStore"].Single(), (p, s) =>
{
p.ConfigureAutoMap(s);
ConfigureOpenIddict(s, p);
p.AddServices(services);
});


ConfigureAuthorizationPolicy(services);
ConfigureBranding(services);
ConfigureQuartz(services);
}

Expand Down Expand Up @@ -135,13 +139,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
}

app.UsePathBase("/pauth");
app.UseSerilogRequestLogging();

app.UseSerilogRequestLogging();

//app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();

app.UseRouting();
app.UseCors();

Expand All @@ -153,11 +157,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapRazorComponents<App>();
endpoints.MapRazorComponents<App>();
endpoints.MapFallbackToFile("index.html");

});
}
}

/// <summary>
/// Configure the Cors so that different clients can consume api
Expand Down Expand Up @@ -222,15 +226,16 @@ private void ConfigureAuthorizationPolicy(IServiceCollection services)
private void ConfigureOpenIddict(IServiceCollection services, IDataStoreConfigurator configurator)
{
//Configure Identity will call services.AddIdentity which will AddAuthentication
configurator.ConfigureIdentity(this.Configuration, services)
configurator.ConfigureIdentity(this.Configuration, services)
.AddSignInManager()
.AddDefaultTokenProviders();

services.ConfigureApplicationCookie(opts => {
opts.LoginPath = "/Identity/Account/Login";
});
services.ConfigureApplicationCookie(opts =>
{
opts.LoginPath = "/Identity/Account/Login";
});

var openIdBuilder = services.AddOpenIddict()
var openIdBuilder = services.AddOpenIddict()
// Register the OpenIddict server components.
.AddServer(options =>
{
Expand Down Expand Up @@ -322,5 +327,10 @@ private void ConfigureQuartz(IServiceCollection services)
// Register the Quartz.NET service and configure it to block shutdown until jobs are complete.
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
}

private static void ConfigureBranding(IServiceCollection services)
{
services.AddScoped<IBrandingService, AppSettingsBrandService>();
}
}
}
8 changes: 8 additions & 0 deletions src/Pixel.Identity.Shared/Branding/Brand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System;

namespace Pixel.Identity.Shared.Branding;

public record Brand(String Name, String ShortName, String LogoUriDark, String LogoUriLight)
{
public static readonly Brand Empty = new(BrandingDefaults.PleaseWait, BrandingDefaults.PleaseWait, String.Empty, String.Empty);
}
12 changes: 12 additions & 0 deletions src/Pixel.Identity.Shared/Branding/BrandingDefaults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace Pixel.Identity.Shared.Branding;

public static class BrandingDefaults
{
public static readonly String PleaseWait = "Please wait ...";
public const String Name = "Pixel Identity Provider";
public const String ShortName = "Pixel Identity";
public const String LogoUriDark = "";
public const String LogoUriLight = "";
}
11 changes: 11 additions & 0 deletions src/Pixel.Identity.Shared/Branding/BrandingProperties.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;

namespace Pixel.Identity.Shared.Branding;

public static class BrandingProperties
{
public static String Name { get; set; } = BrandingDefaults.Name;
public static String ShortName { get; set; } = BrandingDefaults.ShortName;
public static String LogoUriDark { get; set; } = BrandingDefaults.LogoUriDark;
public static String LogoUriLight { get; set; } = BrandingDefaults.LogoUriLight;
}
8 changes: 8 additions & 0 deletions src/Pixel.Identity.Shared/Branding/IBrandingService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Threading.Tasks;

namespace Pixel.Identity.Shared.Branding;

public interface IBrandingService
{
Task<Brand> GetBrandAsync();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@page "/Account/Logins"
@page "/Account/Password"
@using Pixel.Identity.Shared.Models
@attribute [Authorize]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

namespace Pixel.Identity.UI.Client.Pages.Account
{
public partial class Logins : ComponentBase
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to rename it given this page is used to manage external logins ? Should the file name be changed instead to Logins.* ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it does not handle external logins, it shows the change password form.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simply put it allows to manage your logins. If there is no external logins setup, it will let you manage your password for local account login.

public partial class Password : ComponentBase
{
[Inject]
public ISnackbar SnackBar { get; set; }

[Inject]
public IAccountService AccountService { get; set; }

Expand All @@ -26,7 +26,7 @@ public partial class Logins : ComponentBase
protected override async Task OnInitializedAsync()
{
hasLocalAccount = await AccountService.GetHasPasswordAsync();
var userExternalLogins = await ExternalLoginsService.GetExternalLoginsAsync();
var userExternalLogins = await ExternalLoginsService.GetExternalLoginsAsync();
this.externalLogins.AddRange(userExternalLogins);
await base.OnInitializedAsync();
}
Expand Down
5 changes: 5 additions & 0 deletions src/Pixel.Identity.UI.Client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using MudBlazor;
using MudBlazor.Services;
using Pixel.Identity.Shared;
using Pixel.Identity.Shared.Branding;
using Pixel.Identity.Shared.ViewModels;
using Pixel.Identity.UI.Client.Services;
using Pixel.Identity.UI.Client.Validations;
Expand Down Expand Up @@ -65,6 +66,10 @@ public static async Task Main(string[] args)
client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddHttpClient<IBrandingService, RemoteBrandService>(
client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddTransient<IValidator<ApplicationViewModel>, ApplicationDescriptionValidator>();
builder.Services.AddTransient<IValidator<ScopeViewModel>, ScopeValidator>();
builder.Services.AddTransient<IValidator<UserRoleViewModel>, UserRoleValidator>();
Expand Down
22 changes: 22 additions & 0 deletions src/Pixel.Identity.UI.Client/Services/RemoteBrandService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Pixel.Identity.Shared.Branding;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;

namespace Pixel.Identity.UI.Client.Services;

public class RemoteBrandService(HttpClient httpClient) : IBrandingService
{
private readonly HttpClient httpClient = httpClient;
private Brand _brand;

public async Task<Brand> GetBrandAsync()
{
if (_brand == null)
{
_brand = await httpClient.GetFromJsonAsync<Brand>("api/branding");
}

return _brand;
}
}
2 changes: 1 addition & 1 deletion src/Pixel.Identity.UI.Client/Shared/LoginDisplay.razor
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<MudAvatar>@context.User.Identity.Name.First()</MudAvatar>
</ActivatorContent>
<ChildContent>
<MudMenuItem id="profileMenuItem" Href="./authentication/profile">Profile</MudMenuItem>
<MudMenuItem id="profileMenuItem" Href="account/profile">Profile</MudMenuItem>
<MudMenuItem id="signOutMenuItem" @onclick="BeginSignOut">Sign Out</MudMenuItem>
</ChildContent>
</MudMenu>
Expand Down
14 changes: 11 additions & 3 deletions src/Pixel.Identity.UI.Client/Shared/MainLayout.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
@inherits LayoutComponentBase
@using Pixel.Identity.Shared.Branding
@inherits LayoutComponentBase
@inject IBrandingService BrandingService

<MudThemeProvider />
<MudDialogProvider />
Expand All @@ -10,10 +12,10 @@
<MudIconButton Icon="@Icons.Material.Outlined.Menu" Color="Color.Inherit" OnClick="@((e) => DrawerToggle())" />
</MudToolBar>
<MudHidden Breakpoint="Breakpoint.Xs">
<MudText Typo="Typo.h6" Class="ml-4">Pixel Identity</MudText>
<MudText Typo="Typo.h6" Class="ml-4">@_brand.Name</MudText>
</MudHidden>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if it is possible to separate header in to a component and allow users to setup a custom one which can be loaded dynamically at runtime (e.g. by mounting a path on container). Also, the logos are not plugged in yet and would require them to be hosted somewhere else.
I will try to explore this a little.

<MudHidden Breakpoint="Breakpoint.Xs" Invert="true">
<MudText Typo="Typo.subtitle2">Identity Provider</MudText>
<MudText Typo="Typo.subtitle2">@_brand.ShortName</MudText>
</MudHidden>
<MudSpacer />
<LoginDisplay />
Expand All @@ -33,10 +35,16 @@
@code {

public bool _drawerOpen = true;
public Brand _brand = Brand.Empty;

void DrawerToggle()
{
_drawerOpen = !_drawerOpen;
}

protected override async Task OnInitializedAsync()
{
_brand = await BrandingService.GetBrandAsync();
}
}

2 changes: 1 addition & 1 deletion src/Pixel.Identity.UI.Client/Shared/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<AuthorizeView>
<MudNavGroup Title="Account" Expanded="false" Icon="@Icons.Material.Outlined.Dashboard">
<MudNavLink Href="./account/profile">Profile</MudNavLink>
<MudNavLink Href="./account/logins">Logins</MudNavLink>
<MudNavLink Href="./account/password">Password</MudNavLink>
<MudNavLink Href="./account/authenticator/manage">Authenticator</MudNavLink>
</MudNavGroup>
</AuthorizeView>
Expand Down
Loading