Skip to content

Commit

Permalink
Add UI for adding a user (#736)
Browse files Browse the repository at this point in the history
* Add UI for adding a user

* Start the test hosts immediately from constructor
  • Loading branch information
gunndabad authored Aug 14, 2023
1 parent c634238 commit 1a9ca51
Show file tree
Hide file tree
Showing 30 changed files with 856 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using TeachingRecordSystem.Core;
using TeachingRecordSystem.Core.DataStore.Postgres;
using TeachingRecordSystem.Core.DataStore.Postgres.Models;

namespace TeachingRecordSystem.Cli;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public void Configure(EntityTypeBuilder<User> builder)
builder.ToTable("users");
builder.HasKey(e => e.UserId);
builder.Property(e => e.UserType).IsRequired();
builder.Property(e => e.Name).IsRequired().HasMaxLength(200);
builder.Property(e => e.Name).IsRequired().HasMaxLength(User.NameMaxLength);
builder.Property(e => e.Email).HasMaxLength(200).UseCollation("case_insensitive");
builder.Property(e => e.AzureAdUserId).HasMaxLength(100);
builder.Property(e => e.Roles).HasColumnType("varchar[]");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

public class User
{
public const int NameMaxLength = 200;

public required Guid UserId { get; init; }
public required bool Active { get; set; }
public required UserType UserType { get; init; }
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using TeachingRecordSystem.Core.DataStore.Postgres.Models;
using TeachingRecordSystem.Core.Events;
using User = TeachingRecordSystem.Core.DataStore.Postgres.Models.User;

namespace TeachingRecordSystem.Core.DataStore.Postgres;

Expand Down
21 changes: 21 additions & 0 deletions TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/User.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace TeachingRecordSystem.Core.Events;

public record User
{
public required Guid UserId { get; init; }
public required UserType UserType { get; init; }
public required string Name { get; init; }
public string? Email { get; init; }
public string? AzureAdUserId { get; init; }
public required string[] Roles { get; init; }

public static User FromModel(DataStore.Postgres.Models.User user) => new()
{
UserId = user.UserId,
UserType = user.UserType,
Name = user.Name,
Email = user.Email,
AzureAdUserId = user.AzureAdUserId,
Roles = user.Roles,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace TeachingRecordSystem.Core.Events;

public record UserAddedEvent : EventBase
{
public required User User { get; init; }
public required Guid AddedByUserId { get; init; }
}
17 changes: 16 additions & 1 deletion TeachingRecordSystem/src/TeachingRecordSystem.Core/UserRoles.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
namespace TeachingRecordSystem.Core;
using System.ComponentModel.DataAnnotations;
using System.Reflection;

namespace TeachingRecordSystem.Core;

public static class UserRoles
{
[Display(Name = "Administrator")]
public const string Administrator = "Administrator";

public static IReadOnlyCollection<string> All { get; } = new[]
{
Administrator
};

public static string GetDisplayNameForRole(string role)
{
var member = typeof(UserRoles).GetField(role) ?? throw new ArgumentException("Invalid role.", nameof(role));
return member.GetCustomAttribute<DisplayAttribute>()?.Name ?? member.Name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace TeachingRecordSystem.Core;

public enum UserType
{
Person = 1,
Application = 2
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Security.Claims;
using TeachingRecordSystem.SupportUi.Infrastructure.Security;

namespace TeachingRecordSystem.SupportUi;

public static class ClaimsPrincipalExtensions
{
public static Guid GetUserId(this ClaimsPrincipal principal) =>
Guid.Parse(principal.FindFirstValue(CustomClaims.UserId) ?? throw new InvalidOperationException("UserId claim was not found."));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@page "/users/add/confirm"
@using TeachingRecordSystem.Core;
@model TeachingRecordSystem.SupportUi.Pages.Users.AddUser.ConfirmModel
@{
ViewBag.Title = "Add user";
}

<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<form trs-action="l => l.AddUser(Model.UserId!)">
<h1 class="govuk-heading-l">@ViewBag.Title</h1>

<govuk-input asp-for="Email" type="email" disabled />

<govuk-input asp-for="Name" />

<govuk-checkboxes asp-for="Roles">
<govuk-checkboxes-fieldset>
@foreach (var role in UserRoles.All)
{
<govuk-checkboxes-item value="@role">@UserRoles.GetDisplayNameForRole(role)</govuk-checkboxes-item>
}
</govuk-checkboxes-fieldset>
</govuk-checkboxes>

<govuk-button type="submit">Add user</govuk-button>
</form>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TeachingRecordSystem.Core;
using TeachingRecordSystem.Core.DataStore.Postgres;
using TeachingRecordSystem.Core.Events;
using TeachingRecordSystem.SupportUi.Services.AzureActiveDirectory;

namespace TeachingRecordSystem.SupportUi.Pages.Users.AddUser;

public class ConfirmModel : PageModel
{
private readonly TrsDbContext _dbContext;
private readonly IUserService _userService;
private readonly IClock _clock;
private readonly TrsLinkGenerator _linkGenerator;
private Services.AzureActiveDirectory.User? _user;

public ConfirmModel(
TrsDbContext dbContext,
IUserService userService,
IClock clock,
TrsLinkGenerator linkGenerator)
{
_dbContext = dbContext;
_userService = userService;
_clock = clock;
_linkGenerator = linkGenerator;
}

[BindProperty(SupportsGet = true)]
public string? UserId { get; set; }

public string? Email { get; set; }

[BindProperty]
[Display(Name = "Name")]
[Required(ErrorMessage = "Enter a name")]
[MaxLength(Core.DataStore.Postgres.Models.User.NameMaxLength, ErrorMessage = "Name must be 200 characters or less")]
public string? Name { get; set; }

[BindProperty]
[Display(Name = "Roles")]
public string[]? Roles { get; set; }

public IActionResult OnGet()
{
Name = _user!.Name;

return Page();
}

public async Task<IActionResult> OnPost()
{
var roles = Roles ?? Array.Empty<string>();

// Ensure submitted roles are valid
if (roles.Any(r => !UserRoles.All.Contains(r)))
{
return BadRequest();
}

if (roles.Length == 0)
{
ModelState.AddModelError(nameof(Roles), "Select at least one role");
}

if (!ModelState.IsValid)
{
return this.PageWithErrors();
}

var newUser = new Core.DataStore.Postgres.Models.User()
{
Active = true,
AzureAdUserId = _user!.UserId,
Email = _user.Email,
Name = Name!,
Roles = roles,
UserId = Guid.NewGuid(),
UserType = UserType.Person
};

_dbContext.Users.Add(newUser);

_dbContext.AddEvent(new UserAddedEvent()
{
User = Core.Events.User.FromModel(newUser),
AddedByUserId = User.GetUserId(),
CreatedUtc = _clock.UtcNow
});

await _dbContext.SaveChangesAsync();

TempData.SetFlashSuccess("User added");
return Redirect(_linkGenerator.Users());
}

public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
{
_user = await _userService.GetUserById(UserId!);

if (_user is null)
{
context.Result = NotFound();
return;
}

Email = _user.Email;

await next();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@page "/users/add"
@model TeachingRecordSystem.SupportUi.Pages.Users.AddUser.IndexModel
@{
ViewBag.Title = "Add user";
}

<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<form trs-action="l => l.AddUser()">
<h1 class="govuk-heading-l">@ViewBag.Title</h1>

<govuk-input asp-for="Email" type="email" autocomplete="off" />

<govuk-button type="submit">Find user</govuk-button>
</form>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TeachingRecordSystem.Core;
using TeachingRecordSystem.SupportUi.Services.AzureActiveDirectory;

namespace TeachingRecordSystem.SupportUi.Pages.Users.AddUser;

[Authorize(Roles = UserRoles.Administrator)]
public class IndexModel : PageModel
{
private readonly IUserService _userService;
private readonly TrsLinkGenerator _trsLinkGenerator;

public IndexModel(IUserService userService, TrsLinkGenerator trsLinkGenerator)
{
_userService = userService;
_trsLinkGenerator = trsLinkGenerator;
}

[Display(Name = "Email address")]
[Required(ErrorMessage = "Enter an email address")]
[BindProperty]
public string? Email { get; set; }

public void OnGet()
{
}

public async Task<IActionResult> OnPost()
{
if (!ModelState.IsValid)
{
return this.PageWithErrors();
}

var email = Email!;
if (!email.Contains('@'))
{
email += "@education.gov.uk";
}

var user = await _userService.GetUserByEmail(email);

if (user is null)
{
ModelState.AddModelError(nameof(Email), "User does not exist");
return this.PageWithErrors();
}

return Redirect(_trsLinkGenerator.AddUser(user.UserId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@page "/users"
@model TeachingRecordSystem.SupportUi.Pages.Users.IndexModel
@{
ViewBag.Title = "Users";
}

<a href="@LinkGenerator.AddUser()" class="govuk-link">Add user</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace TeachingRecordSystem.SupportUi.Pages.Users;

public class IndexModel : PageModel
{
public void OnGet()
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using TeachingRecordSystem.SupportUi.Infrastructure;
using TeachingRecordSystem.SupportUi.Infrastructure.Filters;
using TeachingRecordSystem.SupportUi.Infrastructure.Security;
using TeachingRecordSystem.SupportUi.Services;

var builder = WebApplication.CreateBuilder(args);

Expand Down Expand Up @@ -156,7 +157,9 @@

builder.Services
.AddTransient<TrsLinkGenerator>()
.AddTransient<CheckUserExistsFilter>();
.AddTransient<CheckUserExistsFilter>()
.AddSingleton<IClock, Clock>()
.AddSupportUiServices(builder.Configuration, builder.Environment);

var app = builder.Build();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace TeachingRecordSystem.SupportUi.Services.AzureActiveDirectory;

public interface IUserService
{
Task<User?> GetUserByEmail(string email);
Task<User?> GetUserById(string userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using TeachingRecordSystem.Core;

namespace TeachingRecordSystem.SupportUi.Services.AzureActiveDirectory;

public static class ServiceCollectionExtensions
{
public static IServiceCollection AddAzureActiveDirectory(
this IServiceCollection services,
IHostEnvironment environment)
{
if (!environment.IsUnitTests())
{
services.AddTransient<IUserService, UserService>();
}

return services;
}
}
Loading

0 comments on commit 1a9ca51

Please sign in to comment.