First Commit

This commit is contained in:
2026-03-18 01:14:49 +01:00
commit 28719fe4d1
39 changed files with 1361 additions and 0 deletions
@@ -0,0 +1,15 @@
namespace Musify.Infrastructure.Identity
{
public class KeycloakConfiguration
{
public const string SectionName = "Keycloak";
public string BaseUrl { get; set; } = string.Empty;
public string Realm { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
}
@@ -0,0 +1,61 @@
using Ardalis.Result;
using Keycloak.Net;
using Keycloak.Net.Models.Users;
using Microsoft.Extensions.Logging;
using Musify.Application.Contracts.Infrastructure;
using Musify.Application.Users.Contracts;
namespace Musify.Infrastructure.Identity
{
public class KeycloakIdentityService(
KeycloakClient keycloakClient,
KeycloakConfiguration keycloakConfiguration,
ILogger<KeycloakIdentityService> logger) : IKeycloakUserClient
{
public async Task<Result<KeycloakUserResponse>> GetUserByIdAsync(string keycloakId, CancellationToken cancellationToken)
{
User keycloakUser;
try
{
keycloakUser = await keycloakClient.GetUserAsync(keycloakConfiguration.Realm, keycloakId, cancellationToken);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exception)
{
logger.LogWarning(exception, "Keycloak user with id {KeycloakId} could not be retrieved.", keycloakId);
return Result.NotFound();
}
return Result.Success(KeycloakUserMapper.Map(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(KeycloakUserMapper.Map)
.ToArray();
}
public Task<int> GetUsersCountAsync(CancellationToken cancellationToken, string search = "", string username = "")
{
return keycloakClient.GetUsersCountAsync(
keycloakConfiguration.Realm,
search: search,
username: username,
cancellationToken: cancellationToken);
}
}
}
@@ -0,0 +1,17 @@
using Keycloak.Net.Models.Users;
using Musify.Application.Users.Contracts;
namespace Musify.Infrastructure.Identity
{
public static class KeycloakUserMapper
{
public static KeycloakUserResponse Map(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."));
}
}
}
+21
View File
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Musify.Infrastructure</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="4.0.19" />
<PackageReference Include="Keycloak.Net.Core" Version="1.0.38" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Application\Application.csproj" />
<ProjectReference Include="..\Musify.Backend\Domain.csproj" />
</ItemGroup>
</Project>
+18
View File
@@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore;
using Musify.Application.Contracts.Infrastructure;
using Musify.Domain.Entities;
namespace Musify.Infrastructure.Persistence
{
public class Database(DbContextOptions<Database> dbContextOptions)
: DbContext(dbContextOptions), IDatabase
{
public DbSet<User> Users => Set<User>();
public DbSet<Track> Tracks => Set<Track>();
public DbSet<PlayList> PlayLists => Set<PlayList>();
public DbSet<PlayListHasTrack> PlayListHasTracks => Set<PlayListHasTrack>();
}
}
+115
View File
@@ -0,0 +1,115 @@
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.S3.Transfer;
using Ardalis.Result;
using Microsoft.Extensions.Logging;
using Musify.Application.Contracts.Infrastructure;
namespace Musify.Infrastructure.Storage
{
public class StorageHandler(IAmazonS3 amazonS3, ILogger<StorageHandler> logger)
: IStorageHandler
{
public async Task<Result<Stream>> GetFile(string bucketName, string keyName, CancellationToken cancellationToken)
{
var request = new GetObjectRequest
{
BucketName = bucketName,
Key = keyName,
};
logger.LogDebug("Getting file from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName);
try
{
var response = await amazonS3.GetObjectAsync(request, cancellationToken);
logger.LogDebug("Successfully got file from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName);
return Result.Success(response.ResponseStream);
}
catch
{
logger.LogDebug("Failed to get file from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName);
return Result.NotFound();
}
}
public async Task<string> GetUrl(string bucketName, string keyName, TimeSpan expirationTime, CancellationToken cancellationToken)
{
var request = new GetPreSignedUrlRequest
{
BucketName = bucketName,
Key = keyName,
Expires = DateTime.UtcNow + expirationTime,
Protocol = Protocol.HTTP
};
logger.LogDebug("Getting pre-signed URL from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName);
var url = await amazonS3.GetPreSignedURLAsync(request);
return url;
}
public async Task<Result> RemoveFile(string bucketName, string keyName, CancellationToken cancellationToken)
{
var request = new DeleteObjectRequest
{
BucketName = bucketName,
Key = keyName,
};
logger.LogDebug("Removing file from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName);
var response = await amazonS3.DeleteObjectAsync(request, cancellationToken);
return Result.NoContent();
}
public async Task<Result> TransferFiles(string sourceDirectory, string bucketName, string route, CancellationToken cancellationToken)
{
var trasnsferUtility = new TransferUtility(amazonS3);
var files = Directory.GetFiles(sourceDirectory, "*");
foreach (var file in files)
{
string fileName = Path.GetFileName(file);
string filePath = Path.Combine(sourceDirectory, file);
try
{
await trasnsferUtility.UploadAsync(filePath, bucketName, fileName, cancellationToken);
}
catch
{
logger.LogError("Failed to transfer file {FileName} to S3 with bucket name {BucketName} and route {Route}", fileName, bucketName, route);
return Result.Error($"Failed to transfer file {fileName} to S3 with bucket name {bucketName} and route {route}");
}
}
return Result.Success();
}
public async Task<Result<string>> UploadFile(Stream sourceStream, string contentType, string bucketName, string keyName, CancellationToken cancellationToken)
{
var request = new PutObjectRequest
{
BucketName = bucketName,
ContentType = contentType,
InputStream = sourceStream,
Key = keyName
};
try
{
var response = await amazonS3.PutObjectAsync(request, cancellationToken);
}
catch
{
logger.LogError("Failed to upload file to S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName);
return Result.Error($"Failed to upload file to S3 with bucket name {bucketName} and key name {keyName}");
}
return Result.Success(keyName);
}
}
}