Remove keycloak and add missing functions

This commit is contained in:
2026-05-09 19:36:49 +02:00
parent 275ba57c0c
commit 5761f6da69
22 changed files with 89 additions and 313 deletions
@@ -1,4 +1,6 @@
namespace Musify.Application.Contracts.Application using X.PagedList;
namespace Musify.Application.Contracts.Application
{ {
public record PaginatedResponse<T>( public record PaginatedResponse<T>(
IReadOnlyCollection<T> Items, IReadOnlyCollection<T> Items,
@@ -7,5 +9,18 @@
int PageCount, int PageCount,
int TotalItemCount, int TotalItemCount,
bool HasNextPage, bool HasNextPage,
bool HasPreviousPage); bool HasPreviousPage)
{
public static PaginatedResponse<T> FromPagedList(IPagedList<T> pagedList)
{
return new PaginatedResponse<T>(
pagedList.ToArray(),
pagedList.PageNumber,
pagedList.PageSize,
pagedList.PageCount,
pagedList.TotalItemCount,
pagedList.HasNextPage,
pagedList.HasPreviousPage);
}
}
} }
@@ -1,21 +0,0 @@
using Musify.Application.Users.Responses;
namespace Musify.Application.Contracts.Infrastructure
{
public interface IKeycloakUserService
{
Task<KeycloakUserResponse> GetUserByIdAsync(string keycloakId, CancellationToken cancellationToken);
Task<IReadOnlyCollection<KeycloakUserResponse>> GetUsersAsync(
CancellationToken cancellationToken,
string search = "",
int first = 0,
int max = 20,
string username = "");
Task<int> GetUsersCountAsync(
CancellationToken cancellationToken,
string search = "",
string username = "");
}
}
@@ -32,16 +32,7 @@ namespace Musify.Application.PlayLists.Handlers
.Select(p => PlayListResponse.FromEntity(p)) .Select(p => PlayListResponse.FromEntity(p))
.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken);
var response = new PaginatedResponse<PlayListResponse>( return Result.Success(PaginatedResponse<PlayListResponse>.FromPagedList(pagedPlayLists));
pagedPlayLists.ToArray(),
pagedPlayLists.PageNumber,
pagedPlayLists.PageSize,
pagedPlayLists.PageCount,
pagedPlayLists.TotalItemCount,
pagedPlayLists.HasNextPage,
pagedPlayLists.HasPreviousPage);
return Result.Success(response);
} }
} }
} }
@@ -25,16 +25,7 @@ namespace Musify.Application.PlayLists.Handlers
.Select(p => PlayListResponse.FromEntity(p)) .Select(p => PlayListResponse.FromEntity(p))
.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken);
var response = new PaginatedResponse<PlayListResponse>( return Result.Success(PaginatedResponse<PlayListResponse>.FromPagedList(pagedPlayLists));
pagedPlayLists.ToArray(),
pagedPlayLists.PageNumber,
pagedPlayLists.PageSize,
pagedPlayLists.PageCount,
pagedPlayLists.TotalItemCount,
pagedPlayLists.HasNextPage,
pagedPlayLists.HasPreviousPage);
return Result.Success(response);
} }
} }
} }
@@ -42,7 +42,7 @@ namespace Musify.Application.Tracks.Handler
await database.Tracks.AddAsync(trackEntity, cancellationToken); await database.Tracks.AddAsync(trackEntity, cancellationToken);
await database.SaveChangesAsync(cancellationToken); await database.SaveChangesAsync(cancellationToken);
return Result.Success(); return Result.Created(TrackResponse.FromEntity(trackEntity));
} }
async Task<Result> UploadFilesAsync( async Task<Result> UploadFilesAsync(
@@ -33,16 +33,7 @@ namespace Musify.Application.Tracks.Handler
.Select(t => TrackResponse.FromEntity(t.Track)) .Select(t => TrackResponse.FromEntity(t.Track))
.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken);
var response = new PaginatedResponse<TrackResponse>( return Result.Success(PaginatedResponse<TrackResponse>.FromPagedList(pagedTracks));
pagedTracks.ToArray(),
pagedTracks.PageNumber,
pagedTracks.PageSize,
pagedTracks.PageCount,
pagedTracks.TotalItemCount,
pagedTracks.HasNextPage,
pagedTracks.HasPreviousPage);
return Result.Success(response);
} }
} }
} }
@@ -18,7 +18,6 @@ namespace Musify.Application.Tracks.Handler
var pageSize = request.PageSize < 1 ? 10 : request.PageSize; var pageSize = request.PageSize < 1 ? 10 : request.PageSize;
var totalCount = await database.PlayLists.CountAsync(cancellationToken); var totalCount = await database.PlayLists.CountAsync(cancellationToken);
var pagedPlayLists = await database.UserHasTracks var pagedPlayLists = await database.UserHasTracks
.AsNoTracking() .AsNoTracking()
.OrderBy(t => t.Id) .OrderBy(t => t.Id)
@@ -26,16 +25,7 @@ namespace Musify.Application.Tracks.Handler
.Select(t => TrackResponse.FromEntity(t.Track)) .Select(t => TrackResponse.FromEntity(t.Track))
.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken);
var response = new PaginatedResponse<TrackResponse>( return Result.Success(PaginatedResponse<TrackResponse>.FromPagedList(pagedPlayLists));
pagedPlayLists.ToArray(),
pagedPlayLists.PageNumber,
pagedPlayLists.PageSize,
pagedPlayLists.PageCount,
pagedPlayLists.TotalItemCount,
pagedPlayLists.HasNextPage,
pagedPlayLists.HasPreviousPage);
return Result.Success(response);
} }
} }
} }
@@ -5,14 +5,21 @@ using Musify.Domain.Entities;
namespace Musify.Application.Users.Commands namespace Musify.Application.Users.Commands
{ {
public record CreateUserCommand(Guid Id) public record CreateUserCommand(Guid Id,
string Name,
string? FirstName,
string? SecondName)
: ICommand<Result<UserResponse>> : ICommand<Result<UserResponse>>
{ {
public static User ToEntity(CreateUserCommand command) public static User ToEntity(CreateUserCommand command)
{ {
return new User return new User
{ {
Id = command.Id Id = command.Id,
Name = command.Name,
NormalizedName = command.Name.ToUpperInvariant(),
FirstName = command.FirstName,
SecondName = command.SecondName
}; };
} }
} }
@@ -1,8 +0,0 @@
using Ardalis.Result;
using Mediator;
namespace Musify.Application.Users.Commands
{
public record DeleteUserCommand(Guid Id)
: ICommand<Result>;
}
@@ -8,7 +8,7 @@ using Musify.Application.Users.Responses;
namespace Musify.Application.Users.Handlers namespace Musify.Application.Users.Handlers
{ {
public class CreateUserCommandHandler(IDatabase database, IKeycloakUserService keycloakUserClient, ILogger<CreateUserCommandHandler> logger) public class CreateUserCommandHandler(IDatabase database, ILogger<CreateUserCommandHandler> logger)
: ICommandHandler<CreateUserCommand, Result<UserResponse> > : ICommandHandler<CreateUserCommand, Result<UserResponse> >
{ {
public async ValueTask<Result<UserResponse>> Handle(CreateUserCommand request, CancellationToken cancellationToken) public async ValueTask<Result<UserResponse>> Handle(CreateUserCommand request, CancellationToken cancellationToken)
@@ -20,28 +20,12 @@ namespace Musify.Application.Users.Handlers
return Result<UserResponse>.Conflict($"User {request.Id} already exists"); return Result<UserResponse>.Conflict($"User {request.Id} already exists");
} }
KeycloakUserResponse keycloakUser;
try
{
keycloakUser = await keycloakUserClient.GetUserByIdAsync(request.Id.ToString(), cancellationToken);
}
catch
{
return Result.NotFound("Keycloak user not found");
}
var newUser = CreateUserCommand.ToEntity(request); var newUser = CreateUserCommand.ToEntity(request);
await database.Users.AddAsync(newUser, cancellationToken); await database.Users.AddAsync(newUser, cancellationToken);
await database.SaveChangesAsync(cancellationToken); await database.SaveChangesAsync(cancellationToken);
logger.LogInformation("Created user {UserId}", newUser.Id); logger.LogInformation("Created user {UserId}", newUser.Id);
return Result.Created(new UserResponse( return Result.Created(UserResponse.FromEntity(newUser));
newUser.Id.ToString(),
keycloakUser.Name,
keycloakUser.FirstName,
keycloakUser.SecondName,
newUser.CreatedAt,
newUser.UpdatedAt));
} }
} }
} }
@@ -1,22 +0,0 @@
using Ardalis.Result;
using Mediator;
using Musify.Application.Contracts.Infrastructure;
namespace Musify.Application.Users.Commands
{
public class DeleteUserCommandHandler(IDatabase database)
: ICommandHandler<DeleteUserCommand, Result>
{
public async ValueTask<Result> Handle(DeleteUserCommand request, CancellationToken cancellationToken)
{
var existingUser = await database.Users.FindAsync(request.Id, cancellationToken);
if (existingUser is null)
return Result.NotFound();
database.Users.Remove(existingUser);
await database.SaveChangesAsync(cancellationToken);
return Result.NoContent();
}
}
}
@@ -7,34 +7,17 @@ using Musify.Application.Users.Responses;
namespace Musify.Application.Users.Handlers namespace Musify.Application.Users.Handlers
{ {
public class GetUserByIdQueryHandler(IDatabase database, IKeycloakUserService keycloakUserClient) public class GetUserByIdQueryHandler(IDatabase database)
: IQueryHandler<GetUserByIdQuery, Result<UserResponse> > : IQueryHandler<GetUserByIdQuery, Result<UserResponse> >
{ {
public async ValueTask<Result<UserResponse>> Handle(GetUserByIdQuery request, CancellationToken cancellationToken) public async ValueTask<Result<UserResponse>> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
{ {
var user = await database.Users var user = await database.Users
.AsNoTracking() .AsNoTracking()
.Select(u => UserResponse.FromEntity(u))
.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); .SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken);
if (user is null)
return Result.NotFound("User not found");
KeycloakUserResponse keycloakUser; return user ?? Result<UserResponse>.NotFound();
try
{
keycloakUser = await keycloakUserClient.GetUserByIdAsync(user.Id.ToString(), cancellationToken);
}
catch
{
return Result.Error("Keycloak user not found");
}
return Result.Success(new UserResponse(
user.Id.ToString(),
keycloakUser.Name,
keycloakUser.FirstName,
keycloakUser.SecondName,
user.CreatedAt,
user.UpdatedAt));
} }
} }
} }
@@ -7,34 +7,17 @@ using Musify.Application.Users.Responses;
namespace Musify.Application.Users.Handlers namespace Musify.Application.Users.Handlers
{ {
public class GetUserByKeycloakQueryHandler(IDatabase database, IKeycloakUserService keycloakUserClient) public class GetUserByKeycloakQueryHandler(IDatabase database)
: IQueryHandler<GetUserByKeycloakQuery, Result<UserResponse> > : IQueryHandler<GetUserByKeycloakQuery, Result<UserResponse> >
{ {
public async ValueTask<Result<UserResponse>> Handle(GetUserByKeycloakQuery request, CancellationToken cancellationToken) public async ValueTask<Result<UserResponse>> Handle(GetUserByKeycloakQuery request, CancellationToken cancellationToken)
{ {
var user = await database.Users var user = await database.Users
.AsNoTracking() .AsNoTracking()
.Select(u => UserResponse.FromEntity(u))
.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); .SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken);
if (user is null)
return Result.NotFound("User not found");
KeycloakUserResponse keycloakUser; return user ?? Result<UserResponse>.NotFound();
try
{
keycloakUser = await keycloakUserClient.GetUserByIdAsync(user.Id.ToString(), cancellationToken);
}
catch
{
return Result.NotFound("Keycloak user not found");
}
return Result.Success(new UserResponse(
user.Id.ToString(),
keycloakUser.Name,
keycloakUser.FirstName,
keycloakUser.SecondName,
user.CreatedAt,
user.UpdatedAt));
} }
} }
} }
@@ -5,12 +5,12 @@ using Musify.Application.Contracts.Application;
using Musify.Application.Contracts.Infrastructure; using Musify.Application.Contracts.Infrastructure;
using Musify.Application.Users.Queries; using Musify.Application.Users.Queries;
using Musify.Application.Users.Responses; using Musify.Application.Users.Responses;
using X.PagedList; using X.PagedList.EF;
using X.PagedList.Extensions; using X.PagedList.Extensions;
namespace Musify.Application.Users.Handlers namespace Musify.Application.Users.Handlers
{ {
public class GetUsersQueryHandler(IDatabase database, IKeycloakUserService keycloakUserClient) public class GetUsersQueryHandler(IDatabase database)
: IQueryHandler<GetUsersQuery, Result<PaginatedResponse<UserResponse> > > : IQueryHandler<GetUsersQuery, Result<PaginatedResponse<UserResponse> > >
{ {
public async ValueTask<Result<PaginatedResponse<UserResponse> >> Handle(GetUsersQuery request, CancellationToken cancellationToken) public async ValueTask<Result<PaginatedResponse<UserResponse> >> Handle(GetUsersQuery request, CancellationToken cancellationToken)
@@ -19,43 +19,21 @@ namespace Musify.Application.Users.Handlers
var pageSize = request.PageSize < 1 ? 10 : request.PageSize; var pageSize = request.PageSize < 1 ? 10 : request.PageSize;
var first = (pageNumber - 1) * pageSize; var first = (pageNumber - 1) * pageSize;
var keycloakUsers = await keycloakUserClient.GetUsersAsync( var user = database.Users
cancellationToken, .AsNoTracking()
first: first, .AsQueryable();
max: pageSize, if (!string.IsNullOrEmpty(request.UsernameSearch))
search: request.UsernameSearch); {
var normalizedUsername = request.UsernameSearch.Trim().ToUpperInvariant();
user = user.Where(u => u.NormalizedName.Contains(normalizedUsername));
}
var keycloakIds = keycloakUsers.Select(u => u.Id).ToList(); var totalCount = await user.CountAsync(cancellationToken);
var pagedUser = await user
.Select(u => UserResponse.FromEntity(u))
.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken);
var databaseUsers = await database.Users return Result.Success(PaginatedResponse<UserResponse>.FromPagedList(pagedUser));
.Where(u => keycloakIds.Contains(u.Id))
.ToListAsync(cancellationToken);
var users = databaseUsers
.Join(keycloakUsers, db => db.Id, kc => kc.Id,
(databaseSource, keycloakSource) => new UserResponse(
keycloakSource.Id.ToString(),
keycloakSource.Name,
keycloakSource.FirstName,
keycloakSource.SecondName,
databaseSource.CreatedAt,
databaseSource.UpdatedAt))
.ToArray();
var totalItemCount = await keycloakUserClient.GetUsersCountAsync(cancellationToken, search: request.UsernameSearch);
var pagedUsers = new StaticPagedList<UserResponse>(users, pageNumber, pageSize, totalItemCount);
var response = new PaginatedResponse<UserResponse>(
pagedUsers.ToArray(),
pagedUsers.PageNumber,
pagedUsers.PageSize,
pagedUsers.PageCount,
pagedUsers.TotalItemCount,
pagedUsers.HasNextPage,
pagedUsers.HasPreviousPage);
return Result.Success(response);
} }
} }
} }
+19 -5
View File
@@ -1,10 +1,24 @@
namespace Musify.Application.Users.Responses using Musify.Domain.Entities;
namespace Musify.Application.Users.Responses
{ {
public record UserResponse( public record UserResponse(
string Id, Guid Id,
string Name, string Name,
string FirstName, string? FirstName,
string SecondName, string? SecondName,
DateTime CreatedAt, DateTime CreatedAt,
DateTime UpdatedAt); DateTime UpdatedAt)
{
public static UserResponse FromEntity(User user)
{
return new UserResponse(
user.Id,
user.Name,
user.FirstName,
user.SecondName,
user.CreatedAt,
user.UpdatedAt);
}
}
} }
+14
View File
@@ -9,6 +9,20 @@ namespace Musify.Domain.Entities
[Key] [Key]
public required Guid Id { get; set; } public required Guid Id { get; set; }
[Required]
[MaxLength(48)]
public required string Name { get; set; }
[Required]
[MaxLength(48)]
public required string NormalizedName { get; set; }
[MaxLength(48)]
public string? FirstName { get; set; }
[MaxLength(48)]
public string? SecondName { get; set; }
[Required] [Required]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
@@ -6,16 +6,14 @@ namespace Musify.Infrastructure.Configuration
{ {
public const string SectionName = "InfrastructureStorage"; public const string SectionName = "InfrastructureStorage";
[Required]
[Url] [Url]
[Required]
public required string Address { get; set; } public required string Address { get; set; }
[Required] [Required]
[MinLength(3)]
public required string AccessKey { get; set; } public required string AccessKey { get; set; }
[Required] [Required]
[MinLength(8)]
public required string SecretAccessKey { get; set; } public required string SecretAccessKey { get; set; }
[Required] [Required]
@@ -1,21 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Musify.Infrastructure.Configuration
{
public class KeycloakConfiguration
{
public const string SectionName = "Keycloak";
[Required]
public required string Address { get; set; }
[Required]
public required string Realm { get; set; }
[Required]
public required string Username { get; set; }
[Required]
public required string Password { get; set; }
}
}
@@ -6,8 +6,8 @@ namespace Musify.Infrastructure.Configuration
{ {
public const string SectionName = "OpenTelemetry"; public const string SectionName = "OpenTelemetry";
[Required]
[Url] [Url]
[Required]
public required string OtlpEndpoint { get; set; } public required string OtlpEndpoint { get; set; }
} }
} }
-1
View File
@@ -15,7 +15,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="4.0.19" /> <PackageReference Include="AWSSDK.S3" Version="4.0.19" />
<PackageReference Include="Keycloak.Net.Core" Version="1.0.38" />
<PackageReference Include="MassTransit" Version="9.0.1" /> <PackageReference Include="MassTransit" Version="9.0.1" />
<PackageReference Include="MassTransit.RabbitMQ" Version="9.0.1" /> <PackageReference Include="MassTransit.RabbitMQ" Version="9.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
@@ -1,67 +0,0 @@
using Keycloak.Net;
using Keycloak.Net.Models.Users;
using Microsoft.Extensions.Logging;
using Musify.Application.Contracts.Infrastructure;
using Musify.Application.Users.Responses;
using Musify.Infrastructure.Configuration;
namespace Musify.Infrastructure.Services
{
public class KeycloakUserService(KeycloakClient keycloakClient, ILogger<KeycloakUserService> logger,
KeycloakConfiguration keycloakConfiguration) : IKeycloakUserService
{
public async Task<KeycloakUserResponse> GetUserByIdAsync(string keycloakId, CancellationToken cancellationToken)
{
User keycloakUser;
try
{
keycloakUser = await keycloakClient.GetUserAsync(keycloakConfiguration.Realm, keycloakId, cancellationToken);
}
catch (OperationCanceledException)
{
logger.LogWarning("Operation to retrieve Keycloak user {KeycloakId} was cancelled", keycloakId);
throw;
}
catch (Exception exception)
{
logger.LogWarning(exception, "Failed to retrieve Keycloak user {KeycloakId}", keycloakId);
throw;
}
return ToResponse(keycloakUser);
}
public async Task<IReadOnlyCollection<KeycloakUserResponse>> GetUsersAsync(CancellationToken cancellationToken, string search = "", int first = 0, int max = 20, string username = "")
{
var users = await keycloakClient.GetUsersAsync(
realm: keycloakConfiguration.Realm,
first: first,
max: max,
search: search,
username: username,
cancellationToken: cancellationToken);
return users
.Select(ToResponse)
.ToArray();
}
public Task<int> GetUsersCountAsync(CancellationToken cancellationToken, string search = "", string username = "")
{
return keycloakClient.GetUsersCountAsync(
keycloakConfiguration.Realm,
search: search,
username: username,
cancellationToken: cancellationToken);
}
KeycloakUserResponse ToResponse(User keycloakUser)
{
return new KeycloakUserResponse(
Guid.Parse(keycloakUser.Id ?? throw new InvalidOperationException("Keycloak user Id is required.")),
keycloakUser.UserName ?? throw new InvalidOperationException("Keycloak user Name is required."),
keycloakUser.FirstName ?? throw new InvalidOperationException("Keycloak user FirstName is required."),
keycloakUser.LastName ?? throw new InvalidOperationException("Keycloak user SecondName is required."));
}
}
}
@@ -1,5 +1,4 @@
using Amazon.S3; using Amazon.S3;
using Keycloak.Net;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@@ -42,28 +41,6 @@ namespace Musify.Infrastructure.Services
return serviceDescriptors; return serviceDescriptors;
} }
public static IServiceCollection AddKeycloakService(this IServiceCollection serviceDescriptors, IConfiguration configuration)
{
serviceDescriptors.AddOptionsWithValidateOnStart<KeycloakConfiguration>()
.Bind(configuration.GetRequiredSection(KeycloakConfiguration.SectionName))
.ValidateDataAnnotations();
serviceDescriptors.AddSingleton(serviceProvider =>
serviceProvider.GetRequiredService<IOptions<KeycloakConfiguration>>().Value);
serviceDescriptors.AddScoped(serviceProvider =>
{
var keycloakConfiguration = serviceProvider.GetRequiredService<KeycloakConfiguration>();
return new KeycloakClient(
keycloakConfiguration.Address,
keycloakConfiguration.Username,
keycloakConfiguration.Password);
} );
serviceDescriptors.AddScoped<IKeycloakUserService, KeycloakUserService>();
return serviceDescriptors;
}
public static IServiceCollection AddAudioTranscoder(this IServiceCollection serviceDescriptors, IConfiguration configuration) public static IServiceCollection AddAudioTranscoder(this IServiceCollection serviceDescriptors, IConfiguration configuration)
{ {
serviceDescriptors serviceDescriptors