First Commit
This commit is contained in:
+428
@@ -0,0 +1,428 @@
|
|||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
##
|
||||||
|
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.rsuser
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
*.env
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Mono auto generated files
|
||||||
|
mono_crash.*
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
|
||||||
|
[Dd]ebug/x64/
|
||||||
|
[Dd]ebugPublic/x64/
|
||||||
|
[Rr]elease/x64/
|
||||||
|
[Rr]eleases/x64/
|
||||||
|
bin/x64/
|
||||||
|
obj/x64/
|
||||||
|
|
||||||
|
[Dd]ebug/x86/
|
||||||
|
[Dd]ebugPublic/x86/
|
||||||
|
[Rr]elease/x86/
|
||||||
|
[Rr]eleases/x86/
|
||||||
|
bin/x86/
|
||||||
|
obj/x86/
|
||||||
|
|
||||||
|
[Ww][Ii][Nn]32/
|
||||||
|
[Aa][Rr][Mm]/
|
||||||
|
[Aa][Rr][Mm]64/
|
||||||
|
[Aa][Rr][Mm]64[Ee][Cc]/
|
||||||
|
bld/
|
||||||
|
[Oo]bj/
|
||||||
|
[Oo]ut/
|
||||||
|
[Ll]og/
|
||||||
|
[Ll]ogs/
|
||||||
|
|
||||||
|
# Build results on 'Bin' directories
|
||||||
|
**/[Bb]in/*
|
||||||
|
# Uncomment if you have tasks that rely on *.refresh files to move binaries
|
||||||
|
# (https://github.com/github/gitignore/pull/3736)
|
||||||
|
#!**/[Bb]in/*.refresh
|
||||||
|
|
||||||
|
# Visual Studio 2015/2017 cache/options directory
|
||||||
|
.vs/
|
||||||
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
#wwwroot/
|
||||||
|
|
||||||
|
# Visual Studio 2017 auto generated files
|
||||||
|
Generated\ Files/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
*.trx
|
||||||
|
|
||||||
|
# NUnit
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
nunit-*.xml
|
||||||
|
|
||||||
|
# Approval Tests result files
|
||||||
|
*.received.*
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
# Benchmark Results
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
|
|
||||||
|
# .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# ASP.NET Scaffolding
|
||||||
|
ScaffoldingReadMe.txt
|
||||||
|
|
||||||
|
# StyleCop
|
||||||
|
StyleCopReport.xml
|
||||||
|
|
||||||
|
# Files built by Visual Studio
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_h.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.idb
|
||||||
|
*.iobj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.ipdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||||
|
!Directory.Build.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*_wpftmp.csproj
|
||||||
|
*.log
|
||||||
|
*.tlog
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
*.VC.db
|
||||||
|
*.VC.VC.opendb
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# Visual Studio Trace Files
|
||||||
|
*.e2e
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# AxoCover is a Code Coverage Tool
|
||||||
|
.axoCover/*
|
||||||
|
!.axoCover/settings.json
|
||||||
|
|
||||||
|
# Coverlet is a free, cross platform Code Coverage Tool
|
||||||
|
coverage*.json
|
||||||
|
coverage*.xml
|
||||||
|
coverage*.info
|
||||||
|
|
||||||
|
# Visual Studio code coverage results
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
|
# in these scripts will be unencrypted
|
||||||
|
PublishScripts/
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# NuGet Symbol Packages
|
||||||
|
*.snupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/[Pp]ackages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/[Pp]ackages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/[Pp]ackages/repositories.config
|
||||||
|
# NuGet v3's project.json files produces more ignorable files
|
||||||
|
*.nuget.props
|
||||||
|
*.nuget.targets
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Windows Store app package directories and files
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
Package.StoreAssociation.xml
|
||||||
|
_pkginfo.txt
|
||||||
|
*.appx
|
||||||
|
*.appxbundle
|
||||||
|
*.appxupload
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!?*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.jfm
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
# Including strong name files can present a security risk
|
||||||
|
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||||
|
#*.snk
|
||||||
|
|
||||||
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
|
#bower_components/
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
ServiceFabricBackup/
|
||||||
|
*.rptproj.bak
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
*.ndf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
*.rptproj.rsuser
|
||||||
|
*- [Bb]ackup.rdl
|
||||||
|
*- [Bb]ackup ([0-9]).rdl
|
||||||
|
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||||
|
*.vbw
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||||
|
*.dsw
|
||||||
|
*.dsp
|
||||||
|
|
||||||
|
# Visual Studio 6 technical files
|
||||||
|
*.ncb
|
||||||
|
*.aps
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
**/.paket/paket.exe
|
||||||
|
paket-files/
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
**/.fake/
|
||||||
|
|
||||||
|
# CodeRush personal settings
|
||||||
|
**/.cr/personal
|
||||||
|
|
||||||
|
# Python Tools for Visual Studio (PTVS)
|
||||||
|
**/__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Cake - Uncomment if you are using it
|
||||||
|
#tools/**
|
||||||
|
#!tools/packages.config
|
||||||
|
|
||||||
|
# Tabs Studio
|
||||||
|
*.tss
|
||||||
|
|
||||||
|
# Telerik's JustMock configuration file
|
||||||
|
*.jmconfig
|
||||||
|
|
||||||
|
# BizTalk build output
|
||||||
|
*.btp.cs
|
||||||
|
*.btm.cs
|
||||||
|
*.odx.cs
|
||||||
|
*.xsd.cs
|
||||||
|
|
||||||
|
# OpenCover UI analysis results
|
||||||
|
OpenCover/
|
||||||
|
|
||||||
|
# Azure Stream Analytics local run output
|
||||||
|
ASALocalRun/
|
||||||
|
|
||||||
|
# MSBuild Binary and Structured Log
|
||||||
|
*.binlog
|
||||||
|
MSBuild_Logs/
|
||||||
|
|
||||||
|
# AWS SAM Build and Temporary Artifacts folder
|
||||||
|
.aws-sam
|
||||||
|
|
||||||
|
# NVidia Nsight GPU debugger configuration file
|
||||||
|
*.nvuser
|
||||||
|
|
||||||
|
# MFractors (Xamarin productivity tool) working folder
|
||||||
|
**/.mfractor/
|
||||||
|
|
||||||
|
# Local History for Visual Studio
|
||||||
|
**/.localhistory/
|
||||||
|
|
||||||
|
# Visual Studio History (VSHistory) files
|
||||||
|
.vshistory/
|
||||||
|
|
||||||
|
# BeatPulse healthcheck temp database
|
||||||
|
healthchecksdb
|
||||||
|
|
||||||
|
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||||
|
MigrationBackup/
|
||||||
|
|
||||||
|
# Ionide (cross platform F# VS Code tools) working folder
|
||||||
|
**/.ionide/
|
||||||
|
|
||||||
|
# Fody - auto-generated XML schema
|
||||||
|
FodyWeavers.xsd
|
||||||
|
|
||||||
|
# VS Code files for those working on multiple tools
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/*.code-snippets
|
||||||
|
|
||||||
|
# Local History for Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# Built Visual Studio Code Extensions
|
||||||
|
*.vsix
|
||||||
|
|
||||||
|
# Windows Installer files from build outputs
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Musify.Application</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Ardalis.Result" Version="10.1.0" />
|
||||||
|
<PackageReference Include="DispatchR.Mediator" Version="2.1.1" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
|
||||||
|
<PackageReference Include="X.PagedList" Version="10.5.9" />
|
||||||
|
<PackageReference Include="X.PagedList.EF" Version="10.5.9" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Musify.Backend\Domain.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Configuration\" />
|
||||||
|
<Folder Include="Contracts\Application\" />
|
||||||
|
<Folder Include="PlayLists\Commands\CreatePlayList\" />
|
||||||
|
<Folder Include="PlayLists\Commands\DeletePlayList\" />
|
||||||
|
<Folder Include="PlayLists\Commands\UpdatePlayList\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Musify.Application
|
||||||
|
{
|
||||||
|
public static class ApplicationDependencyInjection
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Musify.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Musify.Application.Contracts.Infrastructure
|
||||||
|
{
|
||||||
|
public interface IDatabase
|
||||||
|
{
|
||||||
|
DbSet<User> Users { get; }
|
||||||
|
|
||||||
|
DbSet<PlayList> PlayLists { get; }
|
||||||
|
|
||||||
|
DbSet<Track> Tracks { get; }
|
||||||
|
|
||||||
|
DbSet<PlayListHasTrack> PlayListHasTracks { get; }
|
||||||
|
|
||||||
|
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using Musify.Application.Users.Contracts;
|
||||||
|
|
||||||
|
namespace Musify.Application.Contracts.Infrastructure
|
||||||
|
{
|
||||||
|
public interface IKeycloakUserClient
|
||||||
|
{
|
||||||
|
Task<Result<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 = "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
|
||||||
|
namespace Musify.Application.Contracts.Infrastructure
|
||||||
|
{
|
||||||
|
public interface IStorageHandler
|
||||||
|
{
|
||||||
|
Task<Result<Stream>> GetFile(string bucketName, string keyName, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<string> GetUrl(string bucketName, string keyName, TimeSpan ExpirationTime, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<Result<string>> UploadFile(Stream sourceStream, string contentType, string bucketName, string keyName, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<Result> TransferFiles(string sourceDirectory, string bucketName, string route, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<Result> RemoveFile(string bucketName, string keyName, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Musify.Application.PlayLists.Contracts
|
||||||
|
{
|
||||||
|
public record PaginatedPlayListResponse(
|
||||||
|
IReadOnlyCollection<PlayListResponse> PlayLists,
|
||||||
|
int PageNumber,
|
||||||
|
int PageSize,
|
||||||
|
int PageCount,
|
||||||
|
int TotalItemCount,
|
||||||
|
bool HasNextPage,
|
||||||
|
bool HasPreviousPage
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Musify.Application.PlayLists.Contracts
|
||||||
|
{
|
||||||
|
public record PlayListResponse(
|
||||||
|
Guid Id,
|
||||||
|
string Name,
|
||||||
|
string Description,
|
||||||
|
string ImageName,
|
||||||
|
DateTime CreatedAt,
|
||||||
|
DateTime UpdatedAt);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using DispatchR.Abstractions.Send;
|
||||||
|
using Musify.Application.PlayLists.Contracts;
|
||||||
|
|
||||||
|
namespace Musify.Application.PlayLists.Queries.GetPlayListById
|
||||||
|
{
|
||||||
|
public record GetPlayListByIdQuery(Guid Id)
|
||||||
|
: IRequest<GetPlayListByIdQuery, Result<PlayListResponse>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using DispatchR.Abstractions.Send;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Musify.Application.Contracts.Infrastructure;
|
||||||
|
using Musify.Application.PlayLists.Contracts;
|
||||||
|
|
||||||
|
namespace Musify.Application.PlayLists.Queries.GetPlayListById
|
||||||
|
{
|
||||||
|
public class GetPlayListByIdQueryHandler(IDatabase database)
|
||||||
|
: IRequestHandler<GetPlayListByIdQuery, Task<Result<PlayListResponse>> >
|
||||||
|
{
|
||||||
|
public async Task<Result<PlayListResponse>> Handle(GetPlayListByIdQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var playList = await database.PlayLists
|
||||||
|
.AsNoTracking()
|
||||||
|
.SingleOrDefaultAsync(p => p.Id == request.Id, cancellationToken);
|
||||||
|
|
||||||
|
return playList is not null
|
||||||
|
? Result.Success(new PlayListResponse(
|
||||||
|
playList.Id,
|
||||||
|
playList.Name,
|
||||||
|
playList.Description,
|
||||||
|
playList.ImageName,
|
||||||
|
playList.CreatedDate,
|
||||||
|
playList.UpdatedDate))
|
||||||
|
: Result.NotFound("Playlist not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using DispatchR.Abstractions.Send;
|
||||||
|
using Musify.Application.PlayLists.Contracts;
|
||||||
|
|
||||||
|
namespace Musify.Application.PlayLists.Queries.GetPlayLists
|
||||||
|
{
|
||||||
|
public record GetPlayListsQuery(int PageNumber = 1, int PageSize = 10)
|
||||||
|
: IRequest<GetPlayListsQuery, Result<PaginatedPlayListResponse>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using DispatchR.Abstractions.Send;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Musify.Application.Contracts.Infrastructure;
|
||||||
|
using Musify.Application.PlayLists.Contracts;
|
||||||
|
using X.PagedList.EF;
|
||||||
|
|
||||||
|
namespace Musify.Application.PlayLists.Queries.GetPlayLists
|
||||||
|
{
|
||||||
|
public class GetPlayListsQueryHandler(IDatabase database)
|
||||||
|
: IRequestHandler<GetPlayListsQuery, Task<Result<PaginatedPlayListResponse>> >
|
||||||
|
{
|
||||||
|
public async Task<Result<PaginatedPlayListResponse>> Handle(GetPlayListsQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber;
|
||||||
|
var pageSize = request.PageSize < 1 ? 10 : request.PageSize;
|
||||||
|
|
||||||
|
var totalCount = await database.PlayLists.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var pagedPlayLists = await database.PlayLists
|
||||||
|
.AsNoTracking()
|
||||||
|
.OrderBy(p => p.Id)
|
||||||
|
.Select(p => new PlayListResponse(
|
||||||
|
p.Id,
|
||||||
|
p.Name,
|
||||||
|
p.Description,
|
||||||
|
p.ImageName,
|
||||||
|
p.CreatedDate,
|
||||||
|
p.UpdatedDate))
|
||||||
|
.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken);
|
||||||
|
|
||||||
|
var response = new PaginatedPlayListResponse(
|
||||||
|
pagedPlayLists.ToArray(),
|
||||||
|
pagedPlayLists.PageNumber,
|
||||||
|
pagedPlayLists.PageSize,
|
||||||
|
pagedPlayLists.PageCount,
|
||||||
|
pagedPlayLists.TotalItemCount,
|
||||||
|
pagedPlayLists.HasNextPage,
|
||||||
|
pagedPlayLists.HasPreviousPage);
|
||||||
|
|
||||||
|
return Result.Success(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using DispatchR.Abstractions.Send;
|
||||||
|
using Musify.Application.PlayLists.Contracts;
|
||||||
|
|
||||||
|
namespace Musify.Application.PlayLists.Queries.GetPlayListsByUserId
|
||||||
|
{
|
||||||
|
public record GetPlayListsByUserIdQuery(
|
||||||
|
Guid UserId,
|
||||||
|
string Name = "",
|
||||||
|
int PageNumber = 1,
|
||||||
|
int PageSize = 10)
|
||||||
|
: IRequest<GetPlayListsByUserIdQuery, Result<PaginatedPlayListResponse>>;
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using DispatchR.Abstractions.Send;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Musify.Application.Contracts.Infrastructure;
|
||||||
|
using Musify.Application.PlayLists.Contracts;
|
||||||
|
using X.PagedList.EF;
|
||||||
|
|
||||||
|
namespace Musify.Application.PlayLists.Queries.GetPlayListsByUserId
|
||||||
|
{
|
||||||
|
public class GetPlayListsByUserIdQueryHandler(IDatabase database)
|
||||||
|
: IRequestHandler<GetPlayListsByUserIdQuery, Task<Result<PaginatedPlayListResponse>> >
|
||||||
|
{
|
||||||
|
public async Task<Result<PaginatedPlayListResponse>> Handle(GetPlayListsByUserIdQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber;
|
||||||
|
var pageSize = request.PageSize < 1 ? 10 : request.PageSize;
|
||||||
|
var normalizedName = request.Name.Trim().ToUpperInvariant();
|
||||||
|
|
||||||
|
var playListsQuery = database.PlayLists
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(p => p.UserId == request.UserId);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedName))
|
||||||
|
playListsQuery = playListsQuery.Where(p => p.NormalizedName.Contains(normalizedName));
|
||||||
|
|
||||||
|
var totalCount = await playListsQuery.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var pagedPlayLists = await playListsQuery
|
||||||
|
.OrderBy(p => p.CreatedDate)
|
||||||
|
.Select(p => new PlayListResponse(
|
||||||
|
p.Id,
|
||||||
|
p.Name,
|
||||||
|
p.Description,
|
||||||
|
p.ImageName,
|
||||||
|
p.CreatedDate,
|
||||||
|
p.UpdatedDate))
|
||||||
|
.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken);
|
||||||
|
|
||||||
|
var response = new PaginatedPlayListResponse(
|
||||||
|
pagedPlayLists.ToArray(),
|
||||||
|
pagedPlayLists.PageNumber,
|
||||||
|
pagedPlayLists.PageSize,
|
||||||
|
pagedPlayLists.PageCount,
|
||||||
|
pagedPlayLists.TotalItemCount,
|
||||||
|
pagedPlayLists.HasNextPage,
|
||||||
|
pagedPlayLists.HasPreviousPage);
|
||||||
|
|
||||||
|
return Result.Success(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using DispatchR.Abstractions.Send;
|
||||||
|
using Musify.Application.Users.Contracts;
|
||||||
|
|
||||||
|
namespace Musify.Application.Users.Commands.CreateUser
|
||||||
|
{
|
||||||
|
public record CreateUserCommand(Guid Id)
|
||||||
|
: IRequest<CreateUserCommand, Result<UserResponse>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using DispatchR.Abstractions.Send;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Musify.Application.Contracts.Infrastructure;
|
||||||
|
using Musify.Application.Users.Contracts;
|
||||||
|
using Musify.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Musify.Application.Users.Commands.CreateUser
|
||||||
|
{
|
||||||
|
public class CreateUserCommandHandler(IDatabase database, IKeycloakUserClient keycloakUserClient, ILogger<CreateUserCommandHandler> logger)
|
||||||
|
: IRequestHandler<CreateUserCommand, Task<Result<UserResponse> >>
|
||||||
|
{
|
||||||
|
public async Task<Result<UserResponse>> Handle(CreateUserCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Handling {CommandName} with Id: {UserId}", nameof(CreateUserCommand), request.Id);
|
||||||
|
|
||||||
|
var existingUser = database.Users.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken);
|
||||||
|
|
||||||
|
if (existingUser is not null)
|
||||||
|
return Result<UserResponse>.Conflict($"A user with Id {request.Id} already exists.");
|
||||||
|
|
||||||
|
var keycloakUser = await keycloakUserClient.GetUserByIdAsync(request.Id.ToString(), cancellationToken);
|
||||||
|
|
||||||
|
if (!keycloakUser.IsSuccess)
|
||||||
|
return Result.NotFound("Keycloak user not found.");
|
||||||
|
|
||||||
|
var newUser = new User
|
||||||
|
{
|
||||||
|
Id = request.Id
|
||||||
|
};
|
||||||
|
|
||||||
|
await database.Users.AddAsync(newUser, cancellationToken);
|
||||||
|
|
||||||
|
await database.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Created(new UserResponse(
|
||||||
|
newUser.Id.ToString(),
|
||||||
|
keycloakUser.Value.Name,
|
||||||
|
keycloakUser.Value.FirstName,
|
||||||
|
keycloakUser.Value.SecondName,
|
||||||
|
newUser.CreatedAt,
|
||||||
|
newUser.UpdatedAt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using DispatchR.Abstractions.Send;
|
||||||
|
|
||||||
|
namespace Musify.Application.Users.Commands.DeleteUser
|
||||||
|
{
|
||||||
|
public record DeleteUserCommand(Guid Id)
|
||||||
|
: IRequest<DeleteUserCommand, Result>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using DispatchR.Abstractions.Send;
|
||||||
|
using Musify.Application.Contracts.Infrastructure;
|
||||||
|
|
||||||
|
namespace Musify.Application.Users.Commands.DeleteUser
|
||||||
|
{
|
||||||
|
public class DeleteUserCommandHandler(IDatabase database)
|
||||||
|
: IRequestHandler<DeleteUserCommand, Task<Result>>
|
||||||
|
{
|
||||||
|
public async Task<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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Musify.Application.Users.Contracts
|
||||||
|
{
|
||||||
|
public record KeycloakUserResponse(
|
||||||
|
Guid Id,
|
||||||
|
string Name,
|
||||||
|
string FirstName,
|
||||||
|
string SecondName);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Musify.Application.Users.Contracts
|
||||||
|
{
|
||||||
|
public record PaginatedUserResponse(
|
||||||
|
IReadOnlyCollection<UserResponse> Users,
|
||||||
|
int PageNumber,
|
||||||
|
int PageSize,
|
||||||
|
int PageCount,
|
||||||
|
int TotalItemCount,
|
||||||
|
bool HasNextPage,
|
||||||
|
bool HasPreviousPage
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Musify.Application.Users.Contracts
|
||||||
|
{
|
||||||
|
public record UserResponse(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
string FirstName,
|
||||||
|
string SecondName,
|
||||||
|
DateTime CreatedAt,
|
||||||
|
DateTime UpdatedAt);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using DispatchR.Abstractions.Send;
|
||||||
|
using Musify.Application.Users.Contracts;
|
||||||
|
|
||||||
|
namespace Musify.Application.Users.Queries.GetUserById
|
||||||
|
{
|
||||||
|
public record GetUserByIdQuery(Guid Id)
|
||||||
|
: IRequest<GetUserByIdQuery, Task<Result<UserResponse> >>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using DispatchR.Abstractions.Send;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Musify.Application.Contracts.Infrastructure;
|
||||||
|
using Musify.Application.Users.Contracts;
|
||||||
|
|
||||||
|
namespace Musify.Application.Users.Queries.GetUserById
|
||||||
|
{
|
||||||
|
public class GetUserByIdQueryHandler(IDatabase database, IKeycloakUserClient keycloakUserClient)
|
||||||
|
: IRequestHandler<GetUserByIdQuery, Task<Result<UserResponse>>>
|
||||||
|
{
|
||||||
|
public async Task<Result<UserResponse>> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var user = await database.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken);
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
return Result.NotFound("User not found.");
|
||||||
|
|
||||||
|
var keycloakUser = await keycloakUserClient.GetUserByIdAsync(user.Id.ToString(), cancellationToken);
|
||||||
|
|
||||||
|
if (!keycloakUser.IsSuccess)
|
||||||
|
return Result.NotFound("Keycloak user not found.");
|
||||||
|
|
||||||
|
return Result.Success(new UserResponse(
|
||||||
|
user.Id.ToString(),
|
||||||
|
keycloakUser.Value.Name,
|
||||||
|
keycloakUser.Value.FirstName,
|
||||||
|
keycloakUser.Value.SecondName,
|
||||||
|
user.CreatedAt,
|
||||||
|
user.UpdatedAt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using DispatchR.Abstractions.Send;
|
||||||
|
using Musify.Application.Users.Contracts;
|
||||||
|
|
||||||
|
namespace Musify.Application.Users.Queries.GetUserByKeycloakId
|
||||||
|
{
|
||||||
|
public record GetUserByKeycloakQuery(Guid Id)
|
||||||
|
: IRequest<GetUserByKeycloakQuery, Task<Result<UserResponse> >>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using DispatchR.Abstractions.Send;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Musify.Application.Contracts.Infrastructure;
|
||||||
|
using Musify.Application.Users.Contracts;
|
||||||
|
|
||||||
|
namespace Musify.Application.Users.Queries.GetUserByKeycloakId
|
||||||
|
{
|
||||||
|
public class GetUserByKeycloakQueryHandler(IDatabase database, IKeycloakUserClient keycloakUserClient)
|
||||||
|
: IRequestHandler<GetUserByKeycloakQuery, Task<Result<UserResponse>>>
|
||||||
|
{
|
||||||
|
public async Task<Result<UserResponse>> Handle(GetUserByKeycloakQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var user = await database.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken);
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
return Result.NotFound("User not found.");
|
||||||
|
|
||||||
|
var keycloakUser = await keycloakUserClient.GetUserByIdAsync(user.Id.ToString(), cancellationToken);
|
||||||
|
|
||||||
|
if (!keycloakUser.IsSuccess)
|
||||||
|
return Result.NotFound("Keycloak user not found.");
|
||||||
|
|
||||||
|
return Result.Success(new UserResponse(
|
||||||
|
user.Id.ToString(),
|
||||||
|
keycloakUser.Value.Name,
|
||||||
|
keycloakUser.Value.FirstName,
|
||||||
|
keycloakUser.Value.SecondName,
|
||||||
|
user.CreatedAt,
|
||||||
|
user.UpdatedAt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using DispatchR.Abstractions.Send;
|
||||||
|
using Musify.Application.Users.Contracts;
|
||||||
|
|
||||||
|
namespace Musify.Application.Users.Queries.GetUsers
|
||||||
|
{
|
||||||
|
public record GetUsersCommand(int PageNumber, int PageSize, string UsernameSearch)
|
||||||
|
: IRequest<GetUsersCommand, Task<Result<PaginatedUserResponse>> >;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
using DispatchR.Abstractions.Send;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Musify.Application.Contracts.Infrastructure;
|
||||||
|
using Musify.Application.Users.Contracts;
|
||||||
|
using X.PagedList;
|
||||||
|
using X.PagedList.Extensions;
|
||||||
|
|
||||||
|
namespace Musify.Application.Users.Queries.GetUsers
|
||||||
|
{
|
||||||
|
public class GetUsersQueryHandler(IDatabase database, IKeycloakUserClient keycloakUserClient)
|
||||||
|
: IRequestHandler<GetUsersCommand, Task<Result<PaginatedUserResponse> >>
|
||||||
|
{
|
||||||
|
public async Task<Result<PaginatedUserResponse>> Handle(GetUsersCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber;
|
||||||
|
var pageSize = request.PageSize < 1 ? 10 : request.PageSize;
|
||||||
|
var first = (pageNumber - 1) * pageSize;
|
||||||
|
|
||||||
|
var keycloakUsers = await keycloakUserClient.GetUsersAsync(
|
||||||
|
cancellationToken,
|
||||||
|
first: first,
|
||||||
|
max: pageSize,
|
||||||
|
search: request.UsernameSearch);
|
||||||
|
|
||||||
|
var keycloakIds = keycloakUsers.Select(u => u.Id).ToList();
|
||||||
|
|
||||||
|
var databaseUsers = await database.Users
|
||||||
|
.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 PaginatedUserResponse(
|
||||||
|
pagedUsers.ToArray(),
|
||||||
|
pagedUsers.PageNumber,
|
||||||
|
pagedUsers.PageSize,
|
||||||
|
pagedUsers.PageCount,
|
||||||
|
pagedUsers.TotalItemCount,
|
||||||
|
pagedUsers.HasNextPage,
|
||||||
|
pagedUsers.HasPreviousPage);
|
||||||
|
|
||||||
|
return Result.Success(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Musify.Domain</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Musify.Domain.Entities
|
||||||
|
{
|
||||||
|
#pragma warning disable CS8618
|
||||||
|
[Table("PlayLists")]
|
||||||
|
public class PlayList
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public required Guid UserId { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(50)]
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(50)]
|
||||||
|
public required string NormalizedName { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(254)]
|
||||||
|
public required string Description { get; set; } = "No description was provided.";
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(64)]
|
||||||
|
public required string ImageName { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public DateTime UpdatedDate { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
[ForeignKey(nameof(UserId)) ]
|
||||||
|
public User User { get; set; }
|
||||||
|
|
||||||
|
public ICollection<PlayListHasTrack> PlayListTracks { get; set; } = new List<PlayListHasTrack>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Musify.Domain.Entities
|
||||||
|
{
|
||||||
|
#pragma warning disable CS8618
|
||||||
|
[Table("PlayListHasTrack")]
|
||||||
|
public class PlayListHasTrack
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public required Guid PlayListId { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public required Guid TrackId { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public int Position { get; set; } = 0;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public DateTime AddedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
[ForeignKey(nameof(PlayListId)) ]
|
||||||
|
public PlayList PlayList { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(TrackId)) ]
|
||||||
|
public Track? Track { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Musify.Domain.Entities
|
||||||
|
{
|
||||||
|
#pragma warning disable CS8618
|
||||||
|
public class Track
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(50)]
|
||||||
|
public required string Title { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(50)]
|
||||||
|
public required string NormalizedTitle { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(64)]
|
||||||
|
public required string ImageName { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public DateTime UpdatedDate { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public ICollection<User> Users { get; set; } = new List<User>();
|
||||||
|
|
||||||
|
public ICollection<PlayListHasTrack> PlayListTracks { get; set; } = new List<PlayListHasTrack>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Musify.Domain.Entities
|
||||||
|
{
|
||||||
|
[Table("User")]
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public required Guid Id { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
public ICollection<Track> Tracks { get; set; } = new List<Track>();
|
||||||
|
|
||||||
|
public ICollection<PlayList> PlayLists { get; set; } = new List<PlayList>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="Application/Application.csproj" />
|
||||||
|
<Project Path="Infrastructure/Infrastructure.csproj" />
|
||||||
|
<Project Path="Musify.Backend/Domain.csproj" />
|
||||||
|
</Solution>
|
||||||
Reference in New Issue
Block a user