Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a8cabdb
Add migration to insert a deleted user into the Users table
EinarSalbouvet Jun 8, 2026
6c1954a
Merge branch 'main' into feat/deleted-user
EinarSalbouvet Jul 15, 2026
56a4d7c
Remove deleted user data from Users table and associated migration files
EinarSalbouvet Jul 15, 2026
2fab124
Add migration to insert a "Deleted User" record in the Users table
EinarSalbouvet Jul 15, 2026
ba8f6df
Potential fix for pull request finding
EinarSalbouvet Jul 15, 2026
a3b6663
Potential fix for pull request finding
EinarSalbouvet Jul 15, 2026
81f05d7
Fix SQL update statement in DeleteByIdsAsync to prevent redundant par…
EinarSalbouvet Jul 15, 2026
86ecd1c
Merge branch 'feat/deleted-user' of https://github.com/equinor/prisma…
EinarSalbouvet Jul 15, 2026
2e1c70e
Remove migration for deleted user and update table mappings to use nu…
EinarSalbouvet Jul 22, 2026
139fb8c
Merge branch 'main' into feat/deleted-user
EinarSalbouvet Jul 22, 2026
1018bed
Add migration to insert a deleted user and update table mappings
EinarSalbouvet Jul 22, 2026
621d46b
Add migration to insert a deleted user into the Users table
EinarSalbouvet Jun 8, 2026
1551c63
Remove deleted user data from Users table and associated migration files
EinarSalbouvet Jul 15, 2026
6decc7d
Add migration to insert a "Deleted User" record in the Users table
EinarSalbouvet Jul 15, 2026
6055d4e
Potential fix for pull request finding
EinarSalbouvet Jul 15, 2026
7c0b4bb
Fix SQL update statement in DeleteByIdsAsync to prevent redundant par…
EinarSalbouvet Jul 15, 2026
75713ca
Potential fix for pull request finding
EinarSalbouvet Jul 15, 2026
78a7833
Remove migration for deleted user and update table mappings to use nu…
EinarSalbouvet Jul 22, 2026
266def3
Add migration to insert a deleted user and update table mappings
EinarSalbouvet Jul 22, 2026
e6a3933
Merge branch 'feat/deleted-user' of https://github.com/equinor/prisma…
EinarSalbouvet Aug 13, 2026
16abf5f
Add migration to insert a "Deleted User" record into the Users table
EinarSalbouvet Aug 13, 2026
c8284a3
Merge branch 'main' into feat/deleted-user
EinarSalbouvet Aug 14, 2026
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
13 changes: 13 additions & 0 deletions PrismaDotnetApi/PrismaApi.Api/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,17 @@ public IActionResult AuthPlaceholder(CancellationToken ct = default)
{
return StatusCode(StatusCodes.Status501NotImplemented);
}

[HttpDelete("users/{userId}")]
// <summary>
// Deletes a user. Users can only delete themselves, and the user will be anonymized in the database, but the record will not be deleted for audit purposes.
// </summary>
// <exception cref="InvalidOperationException">Thrown when a user attempts to delete a different user.</exception>
Comment thread
EinarSalbouvet marked this conversation as resolved.
public async Task<IActionResult> DeleteUser([FromRoute] string userId, CancellationToken ct = default)
{
var user = HttpContext.GetLoadedUser();

await _userService.DeleteUserAsync(userId, user, ct);
return NoContent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ public interface IUserService
Task<List<UserOutgoingDto>> SearchUsersAsync(string query);
Task<UserOutgoingDto> GetOrCreateUserFromContextAsync(HttpContext context);
Task<List<UserOutgoingDto>> GetByIdsAsync(IEnumerable<string> ids);
Task DeleteUserAsync(string userId, UserOutgoingDto user, CancellationToken ct = default);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using Microsoft.EntityFrameworkCore;
using PrismaApi.Application.Interfaces.Repositories;
using PrismaApi.Application.Mapping;
using PrismaApi.Domain.Constants;
using PrismaApi.Domain.Dtos;
using PrismaApi.Domain.Entities;
Comment thread
Copilot marked this conversation as resolved.
using PrismaApi.Infrastructure.Context;
using PrismaApi.Infrastructure.Extensions;
using System.Linq.Expressions;
using System.Reflection;

namespace PrismaApi.Application.Repositories;

Expand Down Expand Up @@ -79,4 +81,85 @@ public async Task<User> GetOrAddByUserNameAsync(UserIncomingDto dto, Cancellatio

return user;
}

/// <summary>
/// Deletes users by their ids.
/// Before deleting, it updates all auditable entities that reference the user to point to the deleted user entry.
/// This is done to avoid foreign key constraint violations when deleting a user.
/// Also, deletes all related project roles for the user
/// </summary>
public override async Task DeleteByIdsAsync(IEnumerable<string> ids, Expression<Func<User, bool>>? filterPredicate = null, CancellationToken ct = default)
{
var idsList = ids.ToList();
var entries = await DbContext.Users
.OptionalWhere(filterPredicate)
.Where(u => idsList.Contains(u.Id))
.ToListAsync(cancellationToken: ct);
Comment thread
Copilot marked this conversation as resolved.

if (entries.Count == 0)
return;

var auditableEntityTypes = DbContext.Model.GetEntityTypes()
.Where(t => typeof(AuditableEntity).IsAssignableFrom(t.ClrType) && !t.ClrType.IsAbstract)
.Select(t => t.ClrType)
.Distinct()
.ToList();

foreach (var auditableEntityType in auditableEntityTypes)
{
await UpdateAuditableReferencesByTypeAsync(auditableEntityType, idsList, DomainConstants.DeletedUserId, ct);
}

// delete project roles
var projectRoles = await DbContext.ProjectRoles
.Where(e => idsList.Contains(e.UserId))
.ToListAsync(cancellationToken: ct);
DbContext.ProjectRoles.RemoveRange(projectRoles);

foreach (var entry in entries)
{
DbContext.Users.Remove(entry);
}

await DbContext.SaveChangesAsync(ct);
}
Comment thread
EinarSalbouvet marked this conversation as resolved.

private async Task UpdateAuditableReferencesByTypeAsync(
Type auditableEntityType,
IReadOnlyCollection<string> idsList,
string userId,
CancellationToken ct)
{
// Use reflection to call the generic method with the specific entity type
var UpdateAuditableReferencesByTypeMethod = typeof(UserRepository)
.GetMethod(nameof(UpdateAuditableReferencesByTypeAsync), BindingFlags.NonPublic | BindingFlags.Static)!;
var genericMethod = UpdateAuditableReferencesByTypeMethod.MakeGenericMethod(auditableEntityType);
var updateTask = (Task<int>?)genericMethod.Invoke(null, [DbContext, idsList, userId, ct]);
if (updateTask == null)
{
throw new InvalidOperationException($"Could not execute auditable reference update for type '{auditableEntityType.Name}'.");
}
await updateTask;
}

private static Task<int> UpdateAuditableReferencesByTypeAsync<TEntity>(
AppDbContext dbContext,
List<string> idsList,
string userId,
CancellationToken ct) where TEntity : AuditableEntity
{
// Use ExecuteUpdateAsync to update the CreatedById and UpdatedById properties to userId for all entities of type TEntity
return dbContext
.Set<TEntity>()
.Where(e => idsList.Contains(e.CreatedById) || idsList.Contains(e.UpdatedById))
.ExecuteUpdateAsync(
setters => setters
.SetProperty(
e => e.CreatedById,
e => idsList.Contains(e.CreatedById) ? userId : e.CreatedById)
.SetProperty(
e => e.UpdatedById,
e => idsList.Contains(e.UpdatedById) ? userId : e.UpdatedById),
cancellationToken: ct);
}
}
10 changes: 10 additions & 0 deletions PrismaDotnetApi/PrismaApi.Application/Services/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,14 @@ public Task<UserOutgoingDto> GetOrCreateUserFromContextAsync(HttpContext context

public Task<List<UserOutgoingDto>> SearchUsersAsync(string query)
=> _userProvider.SearchUsersAsync(query);

public async Task DeleteUserAsync(string userId, UserOutgoingDto user, CancellationToken ct = default)
{
// user is the user making the request, and only allows to delete itself.
if (userId != user.Id)
{
throw new InvalidOperationException("Users can only delete themselves.");
}
await _userRepository.DeleteByIdsAsync([userId], ct: ct);
}
}
3 changes: 3 additions & 0 deletions PrismaDotnetApi/PrismaApi.Domain/Constants/DomainConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ public static class DomainConstants
public const int MaxOpacity = 100;
public const int MinOpacity = 0;
public const int DefaultTextSize = 24;
public static readonly string DeletedUserId =
"d008bfdf-fe89-48f3-80a8-777bba4d9bbf";
public static readonly string DeletedUserName = "Deleted User";
public static readonly Guid DefaultValueMetricId =
Guid.Parse("288e0811-7ab6-5d24-b80c-9fa925b848a6");
public static readonly string DefaultValueMetricName = "value";
Expand Down
7 changes: 7 additions & 0 deletions PrismaDotnetApi/PrismaApi.Domain/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,12 @@ public static void OnModelConfiguring(ModelBuilder modelBuilder)
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).HasMaxLength(DomainConstants.MaxShortStringLength);
});
modelBuilder.Entity<User>().HasData(new User
{
Id = DomainConstants.DeletedUserId,
Name = DomainConstants.DeletedUserName,
CreatedAt = new DateTimeOffset(new DateTime(2020, 1, 1, 1, 1, 1, 1, DateTimeKind.Utc).AddTicks(1), TimeSpan.Zero),
UpdatedAt = new DateTimeOffset(new DateTime(2020, 1, 1, 1, 1, 1, 1, DateTimeKind.Utc).AddTicks(2), TimeSpan.Zero)
Comment thread
EinarSalbouvet marked this conversation as resolved.
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ private async Task EnforceMinimumProjectRoles(CancellationToken cancellationToke
var currentFacilitatorCount = await ProjectRoles
.AsNoTracking()
.CountAsync(r => r.ProjectId == projectId &&
r.Role.IsFacilitator(),
r.Role.ToLower() == ProjectRoleType.Facilitator.ToString().ToLower(),
cancellationToken);
Comment thread
EinarSalbouvet marked this conversation as resolved.

if (currentFacilitatorCount - facilitatorsBeingRemoved <= 0)
Expand Down
10 changes: 10 additions & 0 deletions PrismaDotnetApi/PrismaApi.Test/Mocks/TestUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,14 @@ public async Task<List<UserOutgoingDto>> GetByIdsAsync(IEnumerable<string> ids)
var users = await _userRepository.GetByIdsAsync(ids, withTracking: false);
return users.ToOutgoingDtos();
}

public async Task DeleteUserAsync(string userId, UserOutgoingDto user, CancellationToken ct = default)
{
// user is the user making the request, and only allows to delete itself.
if (userId != user.Id)
{
throw new InvalidOperationException("Users can only delete themselves.");
}
await _userRepository.DeleteByIdsAsync([userId], ct: ct);
}
}
Loading
Loading