diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..3dbbcf3e707bbeadeca29139f67694b695a211ab --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..271fe8365b4e7641c2e4488f02c952d082b16e6b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.vscode +.DS_Store +bin +obj +*.http +appsettings.json +Migrations diff --git a/Backend.csproj b/Backend.csproj new file mode 100644 index 0000000000000000000000000000000000000000..309d704c5036830993fa7ab38285857f29e916b9 --- /dev/null +++ b/Backend.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/Backend.sln b/Backend.sln new file mode 100644 index 0000000000000000000000000000000000000000..c7196e72ffea412328899acb9e58d57e5c054bd8 --- /dev/null +++ b/Backend.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backend", "Backend.csproj", "{098C6FA2-0AD6-4DB6-A189-6F70D6E3362B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {098C6FA2-0AD6-4DB6-A189-6F70D6E3362B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {098C6FA2-0AD6-4DB6-A189-6F70D6E3362B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {098C6FA2-0AD6-4DB6-A189-6F70D6E3362B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {098C6FA2-0AD6-4DB6-A189-6F70D6E3362B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A69E6D17-A6C9-4FBB-97A3-7ED53714E982} + EndGlobalSection +EndGlobal diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2febe4cf7c5d75176ab089f70f651ed13a2decc4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 5125 + +ENV ASPNETCORE_URLS=http://0.0.0.0:5125 +# ENV ConnectionStrings__Local="Host=aws-0-eu-central-1.pooler.supabase.com;Port=6543;Database=postgres;Username=postgres.xergfpyauvdbavdyzzbk;Password=009988Ppooii@@@@" +ENV ConnectionStrings__Local="Host=aws-0-eu-central-1.pooler.supabase.com;Port=6543;Database=postgres;Username=postgres.hpbtdersdvvpxrkmhbfe;Password=009988Ppooii@@@@" +ENV JWT__KEY="your-long-secret-key" +ENV JWT__ISSUER="your-issuer" +ENV JWT__AUDIENCE="your-audience" +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY ["Backend.csproj", "./"] +RUN dotnet restore "Backend.csproj" +COPY . . +RUN dotnet build "Backend.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Backend.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Backend.dll"] \ No newline at end of file diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000000000000000000000000000000000000..894e789abcd320251dd8e640c6dd7e91ddd8107c --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:58899", + "sslPort": 44376 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5125", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7097;http://localhost:5125", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000000000000000000000000000000000000..0c208ae9181e5e5717e47ec1bd59368aebc6066e --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/d.cmd b/d.cmd new file mode 100644 index 0000000000000000000000000000000000000000..fd6fc47609791695fa1c3437cab7f3d1555664eb --- /dev/null +++ b/d.cmd @@ -0,0 +1,3 @@ +git add . +git commit -m "first commit" +git push -u origin main \ No newline at end of file diff --git a/d.sh b/d.sh new file mode 100755 index 0000000000000000000000000000000000000000..fd6fc47609791695fa1c3437cab7f3d1555664eb --- /dev/null +++ b/d.sh @@ -0,0 +1,3 @@ +git add . +git commit -m "first commit" +git push -u origin main \ No newline at end of file diff --git a/docker-compose.debug.yml b/docker-compose.debug.yml new file mode 100644 index 0000000000000000000000000000000000000000..1d43ccd12cb0438462e010a63e05e8b2d6895fc8 --- /dev/null +++ b/docker-compose.debug.yml @@ -0,0 +1,18 @@ +# Please refer https://aka.ms/HTTPSinContainer on how to setup an https developer certificate for your ASP.NET Core service. + +version: '3.4' + +services: + backend: + image: backend + build: + context: . + dockerfile: ./Dockerfile + args: + - configuration=Debug + ports: + - 5125:5125 + environment: + - ASPNETCORE_ENVIRONMENT=Development + volumes: + - ~/.vsdbg:c:\remote_debugger:rw diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..b6c1972b1eeb93718964b46de3ad0f0004eb353e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +# Please refer https://aka.ms/HTTPSinContainer on how to setup an https developer certificate for your ASP.NET Core service. + +version: '3.4' + +services: + backend: + image: backend + build: + context: . + dockerfile: ./Dockerfile + ports: + - 5125:5125 diff --git a/p.md b/p.md new file mode 100644 index 0000000000000000000000000000000000000000..54d9b64de229c2a0643bd59954a66adaf993eb19 --- /dev/null +++ b/p.md @@ -0,0 +1,250 @@ +Unhandled exception. System.ArgumentException: Host can't be null + at Npgsql.NpgsqlConnectionStringBuilder.PostProcessAndValidate() + at Npgsql.NpgsqlSlimDataSourceBuilder.PrepareConfiguration() + at Npgsql.NpgsqlSlimDataSourceBuilder.Build() + at Npgsql.NpgsqlDataSourceBuilder.Build() + at Program.<>c__DisplayClass0_0.<
$>b__0(DbContextOptionsBuilder options) in /src/src/Program.cs:line 32 + at Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.<>c__DisplayClass1_0`2.b__0(IServiceProvider _, DbContextOptionsBuilder b) + at Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.CreateDbContextOptions[TContext](IServiceProvider applicationServiceProvider, Action`2 optionsAction) + at Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.<>c__DisplayClass17_0`1.b__0(IServiceProvider p) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.b__0(ServiceProviderEngineScope scope) + at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType) + at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType) + at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider) + at Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.<>c__17`1.b__17_1(IServiceProvider p) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.b__0(ServiceProviderEngineScope scope) + at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType) + at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType) + at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider) + at Program.
$(String[] args) in /src/src/Program.cs:line 111 +==> Exited with status 139 +==> Common ways to troubleshoot your deploy: https://docs.render.com/troubleshooting-deploys +Unhandled exception. System.ArgumentException: Host can't be null + at Npgsql.NpgsqlConnectionStringBuilder.PostProcessAndValidate() + at Npgsql.NpgsqlSlimDataSourceBuilder.PrepareConfiguration() + at Npgsql.NpgsqlSlimDataSourceBuilder.Build() + at Npgsql.NpgsqlDataSourceBuilder.Build() + at Program.<>c__DisplayClass0_0.<
$>b__0(DbContextOptionsBuilder options) in /src/src/Program.cs:line 32 + at Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.<>c__DisplayClass1_0`2.b__0(IServiceProvider _, DbContextOptionsBuilder b) + at Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.CreateDbContextOptions[TContext](IServiceProvider applicationServiceProvider, Action`2 optionsAction) + at Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.<>c__DisplayClass17_0`1.b__0(IServiceProvider p) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.b__0(ServiceProviderEngineScope scope) + at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType) + at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType) + at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider) + at Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.<>c__17`1.b__17_1(IServiceProvider p) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.b__0(ServiceProviderEngineScope scope) + at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType) + at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType) + at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider) + at Program.
$(String[] args) in /src/src/Program.cs:line 111 + + + + FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 5125 + +ENV ASPNETCORE_URLS=http://0.0.0.0:5125 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG configuration=Release +WORKDIR /src +COPY ["Backend.csproj", "./"] +RUN dotnet restore "Backend.csproj" +COPY . . +WORKDIR "/src/." +RUN dotnet build "Backend.csproj" -c $configuration -o /app/build + +FROM build AS publish +ARG configuration=Release + +RUN dotnet publish "Backend.csproj" -c $configuration -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Backend.dll"] + + +using System.Text; +using System.Text.Json.Serialization; +using Backend_Teamwork.src.Database; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Middleware; +using Backend_Teamwork.src.Repository; +using Backend_Teamwork.src.Services.artwork; +using Backend_Teamwork.src.Services.booking; +using Backend_Teamwork.src.Services.category; +using Backend_Teamwork.src.Services.order; +using Backend_Teamwork.src.Services.user; +using Backend_Teamwork.src.Services.workshop; +using Backend_Teamwork.src.Utils; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Npgsql; +using static Backend_Teamwork.src.Entities.User; + +var builder = WebApplication.CreateBuilder(args); + +//connect to database +var dataSourceBuilder = new NpgsqlDataSourceBuilder( + builder.Configuration.GetConnectionString("Local") +); +dataSourceBuilder.MapEnum(); +dataSourceBuilder.MapEnum(); + +//add database connection +builder.Services.AddDbContext(options => +{ + options.UseNpgsql(dataSourceBuilder.Build()); +}); + +//add auto-mapper +builder.Services.AddAutoMapper(typeof(MapperProfile).Assembly); + +//add DI services +builder.Services.AddScoped().AddScoped(); +builder.Services.AddScoped().AddScoped(); +builder.Services.AddScoped().AddScoped(); +builder.Services.AddScoped().AddScoped(); +builder.Services.AddScoped().AddScoped(); +builder.Services.AddScoped().AddScoped(); + +//builder.Services.AddScoped().AddScoped(); + + +//add logic for authentication +builder + .Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(Options => + { + Options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + ValidAudience = builder.Configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]) + ), + }; + }); + +//add logic for athorization +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin")); + options.AddPolicy("CustomerOnly", policy => policy.RequireRole("Customer")); +}); + +// *** add CORS settings *** +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowSpecificOrigin", + builder => builder.WithOrigins("http://localhost:5173") + .AllowAnyHeader() + .AllowAnyMethod()); +}); + + +//add controllers +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder + .Services.AddControllers() + .AddJsonOptions(x => x.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles); + +//add swagger +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); +app.UseRouting(); +app.MapGet("/", () => "Server is running"); + +//Convert to Timestamp format +AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); + +// + +//test database connection +using (var scope = app.Services.CreateScope()) +{ + var dbContext = scope.ServiceProvider.GetRequiredService(); + try + { + if (dbContext.Database.CanConnect()) + { + Console.WriteLine("Database is connected"); + } + else + { + Console.WriteLine("Unable to connect to the database."); + } + } + catch (Exception ex) + { + Console.WriteLine($"Database connection failed: {ex.Message}"); + } +} +app.UseHttpsRedirection(); + +//use middleware +app.UseMiddleware(); +app.UseAuthentication(); +app.UseAuthorization(); + +//use controllers +app.MapControllers(); + +//use swagger +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} +app.Run(); + + + diff --git a/run.cmd b/run.cmd new file mode 100644 index 0000000000000000000000000000000000000000..0a5d8b3a6e067e5b9e412c74e4859794ea57dd13 --- /dev/null +++ b/run.cmd @@ -0,0 +1,2 @@ +cd frontend +npm run dev \ No newline at end of file diff --git a/screenshots/Artify ERD.png b/screenshots/Artify ERD.png new file mode 100644 index 0000000000000000000000000000000000000000..376ab9b847ea2dd2ac3facbffe53b8b69d7f94a0 Binary files /dev/null and b/screenshots/Artify ERD.png differ diff --git a/src/Controllers/ArtworksController.cs b/src/Controllers/ArtworksController.cs new file mode 100644 index 0000000000000000000000000000000000000000..2419cf2427966298d3ad7b1928b31366d10b5edc --- /dev/null +++ b/src/Controllers/ArtworksController.cs @@ -0,0 +1,129 @@ +using System.Security.Claims; +using Backend_Teamwork.src.DTO; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Services.artwork; +using Backend_Teamwork.src.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using static Backend_Teamwork.src.DTO.ArtworkDTO; + +namespace Backend_Teamwork.src.Controllers +{ + [ApiController] + [Route("api/v1/[controller]")] + public class ArtworksController : ControllerBase + { + private readonly IArtworkService _artworkService; + + // Constructor + public ArtworksController(IArtworkService service) + { + _artworkService = service; + } + + // Create + // End-Point: api/v1/artworks + [HttpPost] + [Authorize(Roles = "Artist")] + public async Task> CreateOne( + [FromBody] ArtworkCreateDto createDto + ) + { + // extract user information + var authenticateClaims = HttpContext.User; + // get user id from claim + var userId = authenticateClaims + .FindFirst(c => c.Type == ClaimTypes.NameIdentifier)! + .Value; + // string => guid + var userGuid = new Guid(userId); + + var createdArtwork = await _artworkService.CreateOneAsync(userGuid, createDto); + return Ok(createdArtwork); + } + + // Get all artworks + // End-Point: api/v1/artworks + [HttpGet] + public async Task>>> GetAll( + [FromQuery] PaginationOptions paginationOptions + ) + { + var artworkList = await _artworkService.GetAllAsync(paginationOptions); + var totalCount = await _artworkService.GetArtworkCountAsync(); // Get the total count + var response = new PaginatedResponse> + { + Items = artworkList, + TotalCount = totalCount + }; + return Ok(response); + } + + // Get by artwork id + // End-Point: api/v1/artworks/{id} + [HttpGet("{id}")] + public async Task> GetById([FromRoute] Guid id) + { + var artwork = await _artworkService.GetByIdAsync(id); + return Ok(artwork); + } + + // Get by artist Id + // End-Point: api/v1/artworks/artist/{id} + [HttpGet("artist/{artistId}")] + public async Task>> GetByArtistId( + [FromRoute] Guid artistId + ) + { + var artwork = await _artworkService.GetByArtistIdAsync(artistId); + return Ok(artwork); + } + + // Get count of all artworks + // End-Point: api/v1/artworks/count + [HttpGet("count")] + public async Task> GetArtworkCount() + { + var count = await _artworkService.GetArtworkCountAsync(); + return Ok(count); + } + + // // Get count of artworks by artistId + // // End-Point: api/v1/artworks/artist/{artistId}/count + // [HttpGet("artist/{artistId}/count")] + // public async Task> GetArtworkCountByArtist([FromRoute] Guid artistId) + // { + // var count = await _artworkService.GetArtworkCountByArtistAsync(artistId); + // return Ok(count); + // } + + // Update + // End-Point: api/v1/artworks/{id} + [HttpPut("{id}")] + [Authorize(Roles = "Admin,Artist")] + public async Task UpdateOne( + [FromRoute] Guid id, + [FromBody] ArtworkUpdateDTO updateDTO + ) + { + var artwork = await _artworkService.UpdateOneAsync(id, updateDTO); + return Ok(artwork); + } + + // Delete + // End-Point: api/v1/artworks/{id} + [HttpDelete("{id}")] + [Authorize(Roles = "Admin,Artist")] + public async Task DeleteOne([FromRoute] Guid id) + { + await _artworkService.DeleteOneAsync(id); + return NoContent(); + } + } + + public class PaginatedResponse + { + public T Items { get; set; } + public int TotalCount { get; set; } + } +} diff --git a/src/Controllers/BookingsController.cs b/src/Controllers/BookingsController.cs new file mode 100644 index 0000000000000000000000000000000000000000..3f31777eaf9f50c02dd78b980cd24e1abe862f87 --- /dev/null +++ b/src/Controllers/BookingsController.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text.Json; +using System.Threading.Tasks; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Services.booking; +using Backend_Teamwork.src.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using static Backend_Teamwork.src.DTO.BookingDTO; + +namespace Backend_Teamwork.src.Controllers +{ + [ApiController] + [Route("/api/v1/[controller]")] + public class BookingsController : ControllerBase + { + private readonly IBookingService _bookingService; + + public BookingsController(IBookingService bookingService) + { + _bookingService = bookingService; + } + + [HttpGet] + [Authorize(Roles = "Admin")] + public async Task>> GetBookings() + { + var bookings = await _bookingService.GetAllAsync(); + return Ok(bookings); + } + + [HttpGet("{id}")] + [Authorize(Roles = "Admin,Customer")] + public async Task> GetBookingById([FromRoute] Guid id) + { + var authClaims = HttpContext.User; + var userId = authClaims.FindFirst(c => c.Type == ClaimTypes.NameIdentifier)!.Value; + var userRole = authClaims.FindFirst(c => c.Type == ClaimTypes.Role)!.Value; + var convertedUserId = new Guid(userId); + var booking = await _bookingService.GetByIdAsync(id, convertedUserId, userRole); + return Ok(booking); + } + + [HttpGet("search/{userId:guid}")] + [Authorize(Roles = "Admin")] + public async Task>> GetBookingsByUserId( + [FromRoute] Guid userId + ) + { + var bookings = await _bookingService.GetByUserIdAsync(userId); + return Ok(bookings); + } + + [HttpGet("my-bookings")] + [Authorize(Roles = "Customer")] + public async Task>> GetBookingsByUserId() + { + var authClaims = HttpContext.User; + var userId = authClaims.FindFirst(c => c.Type == ClaimTypes.NameIdentifier)!.Value; + var convertedUserId = new Guid(userId); + var bookings = await _bookingService.GetByUserIdAsync(convertedUserId); + return Ok(bookings); + } + + [HttpGet("search/{status:alpha}")] + [Authorize(Roles = "Admin")] + public async Task>> GetBookingsByStatus( + [FromRoute] string status + ) + { + var bookings = await _bookingService.GetByStatusAsync(status); + return Ok(bookings); + } + + [HttpGet("my-bookings/search/{status}")] + [Authorize(Roles = "Customer")] + public async Task>> GetBookingsByUserIdAndStatus( + [FromRoute] string status + ) + { + var authClaims = HttpContext.User; + var userId = authClaims.FindFirst(c => c.Type == ClaimTypes.NameIdentifier)!.Value; + var convertedUserId = new Guid(userId); + var bookings = await _bookingService.GetByUserIdAndStatusAsync(convertedUserId, status); + return Ok(bookings); + } + + [HttpGet("page")] + [Authorize(Roles = "Admin")] + public async Task>> GetBookingsWithPagination( + [FromQuery] PaginationOptions paginationOptions + ) + { + var bookings = await _bookingService.GetWithPaginationAsync(paginationOptions); + return Ok(bookings); + } + + [HttpGet("my-bookings/page")] + [Authorize(Roles = "Customer")] + public async Task>> GetBookingsByUserIdWithPagination( + [FromQuery] PaginationOptions paginationOptions + ) + { + var authClaims = HttpContext.User; + var userId = authClaims.FindFirst(c => c.Type == ClaimTypes.NameIdentifier)!.Value; + paginationOptions.Search=userId; + var bookings = await _bookingService.GetByUserIdWithPaginationAsync(paginationOptions); + return Ok(bookings); + } + + [HttpPost] + [Authorize(Roles = "Customer")] + public async Task> CreateBooking( + [FromBody] BookingCreateDto bookingDTO + ) + { + var authClaims = HttpContext.User; + var userId = authClaims.FindFirst(c => c.Type == ClaimTypes.NameIdentifier)!.Value; + var convertedUserId = new Guid(userId); + var booking = await _bookingService.CreateAsync(bookingDTO, convertedUserId); + return CreatedAtAction(nameof(CreateBooking), new { id = booking.Id }, booking); + } + + [HttpPut("confirm/{id}")] + [Authorize(Roles = "Admin")] + public async Task> ConfirmBooking([FromRoute] Guid id) + { + var booking = await _bookingService.ConfirmAsync(id); + return Ok(booking); + } + + [HttpPut("reject/{workshopId}")] + [Authorize(Roles = "Admin")] + public async Task>> RejectBooking( + [FromRoute] Guid workshopId + ) + { + var bookings = await _bookingService.RejectAsync(workshopId); + return Ok(bookings); + } + + [HttpPut("my-bookings/cancel/{id}")] + [Authorize(Roles = "Customer")] + public async Task> CancelBooking([FromRoute] Guid id) + { + var authClaims = HttpContext.User; + var userId = authClaims.FindFirst(c => c.Type == ClaimTypes.NameIdentifier)!.Value; + var convertedUserId = new Guid(userId); + var booking = await _bookingService.CancelAsync(id, convertedUserId); + return Ok(booking); + } + } +} diff --git a/src/Controllers/CategoriesController.cs b/src/Controllers/CategoriesController.cs new file mode 100644 index 0000000000000000000000000000000000000000..1210a13aca3aec4e24ddea8bc4606885695352a9 --- /dev/null +++ b/src/Controllers/CategoriesController.cs @@ -0,0 +1,90 @@ +using System.Text.Json; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Services.category; +using Backend_Teamwork.src.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using static Backend_Teamwork.src.DTO.CategoryDTO; + +namespace Backend_Teamwork.src.Controllers +{ + [ApiController] + [Route("/api/v1/[controller]")] + public class CategoriesController : ControllerBase + { + private readonly ICategoryService _categoryService; + + public CategoriesController(ICategoryService categoryService) + { + _categoryService = categoryService; + } + + [HttpGet] + public async Task>> GetCategories() + { + var categories = await _categoryService.GetAllAsync(); + return Ok(categories); + } + + [HttpGet("{id}")] + public async Task> GetCategoryById([FromRoute] Guid id) + { + var category = await _categoryService.GetByIdAsync(id); + return Ok(category); + } + + [HttpGet("search/{name}")] + public async Task> GetCategoryByName([FromRoute] string name) + { + var category = await _categoryService.GetByNameAsync(name); + return Ok(category); + } + + [HttpGet("page")] + public async Task>> GetCategoriesWithPaginationAsync( + [FromQuery] PaginationOptions paginationOptions + ) + { + var categories = await _categoryService.GetWithPaginationAsync(paginationOptions); + return Ok(categories); + } + + [HttpGet("sort")] + public async Task>> SortCategoriesByName() + { + var categories = await _categoryService.SortByNameAsync(); + return Ok(categories); + } + + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task> CreateCategory( + [FromBody] CategoryCreateDto categoryDTO + ) + { + var category = await _categoryService.CreateAsync(categoryDTO); + return CreatedAtAction(nameof(CreateCategory), new { id = category.Id }, category); + } + + [HttpPut("{id}")] + [Authorize(Roles = "Admin")] + public async Task> UpdateCategory( + [FromRoute] Guid id, + [FromBody] CategoryUpdateDto categoryDTO + ) + { + var category = await _categoryService.UpdateAsync(id, categoryDTO); + return Ok(category); + } + + [HttpDelete("{id}")] + [Authorize(Roles = "Admin")] + public async Task DeleteCategory([FromRoute] Guid id) + { + await _categoryService.DeleteAsync(id); + return NoContent(); + } + + + } +} diff --git a/src/Controllers/OrdersController.cs b/src/Controllers/OrdersController.cs new file mode 100644 index 0000000000000000000000000000000000000000..08755a3fdd95b099606809fd08986d5495f9b3f9 --- /dev/null +++ b/src/Controllers/OrdersController.cs @@ -0,0 +1,126 @@ +using System.Security.Claims; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Services.order; +using Backend_Teamwork.src.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using static Backend_Teamwork.src.DTO.OrderDTO; + +namespace Backend_Teamwork.src.Controllers +{ + [Route("api/v1/[controller]")] + [ApiController] + public class OrdersController : ControllerBase + { + private readonly IOrderService _orderService; + + public OrdersController(IOrderService service) + { + _orderService = service; + } + + // GET: api/v1/orders + [HttpGet] + [Authorize(Roles = "Admin")] // Accessible by Admin + public async Task>> GetOrders() + { + var orders = await _orderService.GetAllAsync(); + return Ok(orders); + } + + // GET: api/v1/orders + [HttpGet("my-orders")] + [Authorize(Roles = "Customer")] + public async Task>> GetMyOrders() + { + var authClaims = HttpContext.User; + var userId = authClaims.FindFirst(c => c.Type == ClaimTypes.NameIdentifier)!.Value; + var convertedUserId = new Guid(userId); + var orders = await _orderService.GetAllAsync(convertedUserId); + return Ok(orders); + } + + // GET: api/v1/orders/{id} + [HttpGet("{id}")] + [Authorize(Roles = "Admin")] // Accessible by Admin + public async Task> GetOrderById([FromRoute] Guid id) + { + var order = await _orderService.GetByIdAsync(id); + return Ok(order); + } + + // GET: api/v1/orders/{id} + [HttpGet("my-orders/{id}")] + [Authorize(Roles = "Customer")] + public async Task> GetMyOrderById([FromRoute] Guid id) + { + var authClaims = HttpContext.User; + var userId = authClaims.FindFirst(c => c.Type == ClaimTypes.NameIdentifier)!.Value; + var convertedUserId = new Guid(userId); + var order = await _orderService.GetByIdAsync(id, convertedUserId); + return Ok(order); + } + + // POST: api/v1/orders + [HttpPost("add")] + [Authorize(Roles = "Customer")] + public async Task> AddOrder([FromBody] OrderCreateDto createDto) + { + // extract user information + var authenticateClaims = HttpContext.User; + var userId = authenticateClaims + .FindFirst(c => c.Type == ClaimTypes.NameIdentifier)! + .Value; + var userGuid = new Guid(userId); + + var orderCreated = await _orderService.CreateOneAsync(userGuid, createDto); + + return CreatedAtAction( + nameof(GetOrderById), + new { id = orderCreated.Id }, + orderCreated + ); + } + + // PUT: api/v1/orders/{id} + [HttpPut("{id}")] + [Authorize(Roles = "Admin")] // Accessible by Admin + public async Task> UpdateOrder( + [FromRoute] Guid id, + [FromBody] OrderUpdateDto updateDto + ) + { + var isUpdated = await _orderService.UpdateOneAsync(id, updateDto); + return Ok(isUpdated); + } + + // DELETE: api/v1/orders/{id} + [HttpDelete("{id}")] + [Authorize(Roles = "Admin")] // Accessible by Admin + public async Task> DeleteOrder(Guid id) + { + await _orderService.DeleteOneAsync(id); + return NoContent(); + } + + // Extra Features + // GET: api/v1/users/page + [HttpGet("pagination")] + [Authorize(Roles = "Admin")] // Accessible by Admin + public async Task>> GetOrdersByPage( + [FromQuery] PaginationOptions paginationOptions + ) + { + var orders = await _orderService.GetOrdersByPage(paginationOptions); + return Ok(orders); + } + + [HttpGet("sort-by-date")] + [Authorize(Roles = "Admin")] + public async Task>> SortOrdersByDate() + { + var orders = await _orderService.SortOrdersByDate(); + return Ok(orders); + } + } +} diff --git a/src/Controllers/PaymentsController.cs b/src/Controllers/PaymentsController.cs new file mode 100644 index 0000000000000000000000000000000000000000..f24b9ec6055956d65cad33f81f8bd89868bb4f58 --- /dev/null +++ b/src/Controllers/PaymentsController.cs @@ -0,0 +1,60 @@ +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Services.payment; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using static Backend_Teamwork.src.DTO.PaymentDTO; + +namespace Backend_Teamwork.src.Controllers +{ + [ApiController] + [Route("api/v1/[controller]")] + public class PaymentController : ControllerBase + { + private readonly IPaymentService _paymentService; + + public PaymentController(IPaymentService paymentService) + { + _paymentService = paymentService; + } + + [HttpGet] + [Authorize(Roles = "Admin")] + public async Task>> GetPayments() + { + var payments = await _paymentService.GetAllAsync(); + return Ok(payments); + } + + [HttpGet("{id}")] + [Authorize(Roles = "Admin")] + public async Task> GetPaymentById(Guid id) + { + var payment = await _paymentService.GetByIdAsync(id); + return Ok(payment); + } + + [HttpPost] + [Authorize] + public async Task> CreatePayment(PaymentCreateDTO newPayment) + { + var payment = await _paymentService.CreateOneAsync(newPayment); + return CreatedAtAction(nameof(GetPaymentById), new { id = payment.Id }, payment); + } + + [HttpDelete("{id}")] + [Authorize(Roles = "Admin")] + public async Task DeletePayment(Guid id) + { + await _paymentService.DeleteOneAsync(id); + return NoContent(); + } + + [HttpPut("{id}")] + [Authorize(Roles = "Admin")] + public ActionResult UpdatePayment(Guid id, PaymentUpdateDTO updatedPayment) + { + var payment = _paymentService.UpdateOneAsync(id,updatedPayment); + return Ok(payment); + } + } +} diff --git a/src/Controllers/UsersController.cs b/src/Controllers/UsersController.cs new file mode 100644 index 0000000000000000000000000000000000000000..cbe05753241420a3ce89fe45f224732fd85a8d8f --- /dev/null +++ b/src/Controllers/UsersController.cs @@ -0,0 +1,133 @@ +using System.Security.Claims; +using Backend_Teamwork.src.Services.user; +using Backend_Teamwork.src.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using static Backend_Teamwork.src.DTO.UserDTO; +using static Backend_Teamwork.src.Entities.User; + +namespace Backend_Teamwork.src.Controllers +{ + [ApiController] + [Route("api/v1/[controller]")] + public class UsersController : ControllerBase + { + private readonly IUserService _userService; + + // DI + public UsersController(IUserService service) + { + _userService = service; + } + + // GET: api/v1/users + [HttpGet] + // [Authorize(Roles = "Admin")] // Only Admin + public async Task>> GetUsers( + [FromQuery] PaginationOptions paginationOptions + ) + { + var users = await _userService.GetAllAsync(paginationOptions); + return Ok(users); + } + + [HttpGet("{id:guid}")] + // [Authorize(Roles = "Admin")] // Only Admin + public async Task> GetUserById([FromRoute] Guid id) + { + var user = await _userService.GetByIdAsync(id); + return Ok(user); + } + + [HttpGet("profile")] + // [Authorize] + public async Task> GetInformationById() + { + // Get the user ID from the token claims + var authClaims = HttpContext.User; + var userId = authClaims.FindFirst(c => c.Type == ClaimTypes.NameIdentifier)!.Value; + var convertedUserId = new Guid(userId); + var user = await _userService.GetByIdAsync(convertedUserId); + return Ok(user); + } + + // GET: api/v1/users/email + [HttpGet("email/{email}")] + // [Authorize(Roles = "Admin")] // Only Admin + public async Task> GetByEmail([FromRoute] string email) + { + var user = await _userService.GetByEmailAsync(email); + return Ok(user); + } + + // POST: api/v1/users + [HttpPost] + public async Task> SignUp([FromBody] UserCreateDto createDto) + { + var UserCreated = await _userService.CreateOneAsync(createDto); + return CreatedAtAction(nameof(GetUserById), new { id = UserCreated.Id }, UserCreated); + } + + // POST: api/v1/users/create-admin + [HttpPost("create-admin")] + // [Authorize(Roles = "Admin")] // Only Admin + public async Task> CreateAdmin([FromBody] UserCreateDto createDto) + { + createDto.Role = UserRole.Admin; // Set role as 'Admin' + var adminCreated = await _userService.CreateOneAsync(createDto); + return CreatedAtAction(nameof(GetUserById), new { id = adminCreated.Id }, adminCreated); + } + + // POST: api/v1/users/signin + [HttpPost("signin")] + public async Task> SignIn([FromBody] UserSigninDto signinDto) + { + var token = await _userService.SignInAsync(signinDto); + return Ok(token); + } + + [HttpPut("{id:guid}")] + // [Authorize(Roles = "Admin")] // Only Admin + public async Task> UpdateUser( + [FromRoute] Guid id, + [FromBody] UserUpdateDto updateDto + ) + { + await _userService.UpdateOneAsync(id, updateDto); + return NoContent(); + } // should ask my teammates + + [HttpPut("profile")] + // [Authorize] + public async Task> UpdateProfileInformation( + [FromBody] UserUpdateDto updateDto + ) + { + // Get the user ID from the token claims + var authClaims = HttpContext.User; + var userId = authClaims.FindFirst(c => c.Type == ClaimTypes.NameIdentifier)!.Value; + var convertedUserId = new Guid(userId); + await _userService.UpdateOneAsync(convertedUserId, updateDto); + return NoContent(); + } + + // DELETE: api/v1/users/{id} + [HttpDelete("{id:guid}")] + // [Authorize(Roles = "Admin")] // Only Admin + public async Task> DeleteUser([FromRoute] Guid id) + { + await _userService.DeleteOneAsync(id); + return NoContent(); + } + + // Extra Features + // GET: api/v1/users/count + [HttpGet("count")] + // [Authorize(Roles = "Admin")] // Only Admin + public async Task> GetTotalUsersCount() + { + var count = await _userService.GetTotalUsersCountAsync(); + return Ok(count); + } + } +} diff --git a/src/Controllers/WorkshopsController.cs b/src/Controllers/WorkshopsController.cs new file mode 100644 index 0000000000000000000000000000000000000000..f4761af8c7e45f78a70b0f6425e13febf99e7ef3 --- /dev/null +++ b/src/Controllers/WorkshopsController.cs @@ -0,0 +1,86 @@ +using System.Security.Claims; +using Backend_Teamwork.src.Services.workshop; +using Backend_Teamwork.src.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using static Backend_Teamwork.src.DTO.WorkshopDTO; + +namespace sda_3_online_Backend_Teamwork.src.Controllers +{ + [ApiController] + [Route("api/v1/[controller]")] + public class WorkshopsController : ControllerBase + { + private readonly IWorkshopService _workshopService; + + public WorkshopsController(IWorkshopService service) + { + _workshopService = service; + } + + [HttpGet] + public async Task>> GetWorkshop() + { + var workshops = await _workshopService.GetAllAsync(); + return Ok(workshops); + } + + [HttpGet("{id}")] + public async Task> GetWorkshopById([FromRoute] Guid id) + { + var workshop = await _workshopService.GetByIdAsync(id); + return Ok(workshop); + } + + [HttpGet("page")] + public async Task> GetWorkShopByPage( + [FromQuery] PaginationOptions paginationOptions + ) + { + var workshops = await _workshopService.GetAllAsync(paginationOptions); + return Ok(workshops); + } + + [HttpPost] + [Authorize(Roles = "Admin,Artist")] + public async Task> CreateWorkshop( + [FromBody] WorkshopCreateDTO createDto + ) + { + // extract user information + var authenticateClaims = HttpContext.User; + // get user id from claim + var userId = authenticateClaims + .FindFirst(c => c.Type == ClaimTypes.NameIdentifier)! + .Value; + // string => guid + var userGuid = new Guid(userId); + + var workshopCreated = await _workshopService.CreateOneAsync(userGuid, createDto); + return CreatedAtAction( + nameof(GetWorkshopById), + new { id = workshopCreated.Id }, + workshopCreated + ); + } + + [HttpDelete("{id}")] + [Authorize(Roles = "Admin,Artist")] + public async Task> DeleteWorkshop([FromRoute] Guid id) + { + var isDeleted = await _workshopService.DeleteOneAsync(id); + return NoContent(); + } + + [HttpPut("{id}")] + [Authorize(Roles = "Admin,Artist")] + public async Task> UpdateWorkshop( + Guid id, + [FromBody] WorkshopUpdateDTO updateDto + ) + { + var updateWorkshop = await _workshopService.UpdateOneAsync(id, updateDto); + return Ok(updateWorkshop); + } + } +} diff --git a/src/DTO/ArtworkDTO.cs b/src/DTO/ArtworkDTO.cs new file mode 100644 index 0000000000000000000000000000000000000000..b12b15ef165ac940a7ffca09e436284f4b0b9781 --- /dev/null +++ b/src/DTO/ArtworkDTO.cs @@ -0,0 +1,79 @@ +using System.ComponentModel.DataAnnotations; +using Backend_Teamwork.src.Entities; +using static Backend_Teamwork.src.DTO.UserDTO; + +namespace Backend_Teamwork.src.DTO +{ + public class ArtworkDTO + { + // create Artwork + public class ArtworkCreateDto + { + [ + Required(ErrorMessage = "Title shouldn't be null"), + MinLength(6, ErrorMessage = "Title should be at at least 6 characters"), + MaxLength(30, ErrorMessage = "Title shouldn't be more than 30 characters") + ] + public string Title { get; set; } + + [ + Required(ErrorMessage = "Description shouldn't be null"), + MinLength(30, ErrorMessage = "Description should be at at least 30 characters"), + MaxLength(200, ErrorMessage = "Description shouldn't be more than 200 characters") + ] + public string Description { get; set; } + + [Range(1, int.MaxValue, ErrorMessage = "Quantity should be greater than zero.")] + public int Quantity { get; set; } + + [Range(1.0, double.MaxValue, ErrorMessage = "Price should be greater than zero.")] + public decimal Price { get; set; } + public DateTime? CreatedAt { get; set; } = DateTime.Now; + + [Required(ErrorMessage = "Category Id shouldn't be null")] + public Guid CategoryId { get; set; } + } + + // read data (get data) + public class ArtworkReadDto + { + public Guid Id { get; set; } + public string Title { get; set; } + public string Description { get; set; } + public int Quantity { get; set; } + public decimal Price { get; set; } + public DateTime CreatedAt { get; set; } + public Category Category { get; set; } + public UserReadDto User { get; set; } + } + + // update + public class ArtworkUpdateDTO + { + [ + Required(ErrorMessage = "Title shouldn't be null"), + MinLength(6, ErrorMessage = "Title should be at at least 6 characters"), + MaxLength(30, ErrorMessage = "Title shouldn't be more than 30 characters") + ] + public string Title { get; set; } + + [ + Required(ErrorMessage = "Description shouldn't be null"), + MinLength(30, ErrorMessage = "Description should be at at least 30 characters"), + MaxLength(200, ErrorMessage = "Description shouldn't be more than 200 characters") + ] + public string Description { get; set; } + + [Range(1, int.MaxValue, ErrorMessage = "Quantity should be greater than zero.")] + public int Quantity { get; set; } + + [Range(1.0, double.MaxValue, ErrorMessage = "Price should be greater than zero.")] + public decimal Price { get; set; } + } + public class ArtworkResponseDto + { + public List Artworks { get; set; } + public int TotalCount { get; set; } + } + } +} diff --git a/src/DTO/BookingDTO.cs b/src/DTO/BookingDTO.cs new file mode 100644 index 0000000000000000000000000000000000000000..a80934b75ac4b740930aec04296daab843d13ef7 --- /dev/null +++ b/src/DTO/BookingDTO.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using Backend_Teamwork.src.Entities; + +namespace Backend_Teamwork.src.DTO +{ + public class BookingDTO + { + public class BookingCreateDto + { + [Required(ErrorMessage = "Workshop Id shouldn't be null"),] + public Guid WorkshopId { get; set; } + public DateTime? CreatedAt { get; set; } = DateTime.Now; + } + + public class BookingReadDto + { + public Guid Id { get; set; } + public Status Status { get; set; } + public DateTime CreatedAt { get; set; } + public Workshop Workshop { get; set; } + public User User { get; set; } + } + } +} diff --git a/src/DTO/CategoryDTO.cs b/src/DTO/CategoryDTO.cs new file mode 100644 index 0000000000000000000000000000000000000000..ab872b02aa3fb051aac8177374764bede10c4175 --- /dev/null +++ b/src/DTO/CategoryDTO.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; + +namespace Backend_Teamwork.src.DTO +{ + public class CategoryDTO + { + public class CategoryCreateDto + { + [ + Required(ErrorMessage = "Name shouldn't be null"), + MinLength(2, ErrorMessage = "Name should be at at least 2 characters"), + MaxLength(10, ErrorMessage = "Name shouldn't be more than 10 characters") + ] + public string Name { get; set; } + } + + public class CategoryUpdateDto + { + [ + Required(ErrorMessage = "Name shouldn't be null"), + MinLength(2, ErrorMessage = "Name should be at at least 2 characters"), + MaxLength(10, ErrorMessage = "Name shouldn't be more than 10 characters") + ] + public string Name { get; set; } + } + + public class CategoryReadDto + { + public Guid Id { get; set; } + public string Name { get; set; } + } + } +} diff --git a/src/DTO/OrderDTO.cs b/src/DTO/OrderDTO.cs new file mode 100644 index 0000000000000000000000000000000000000000..072f86328421237f6a4ec44e5a8cfaf93b5310c9 --- /dev/null +++ b/src/DTO/OrderDTO.cs @@ -0,0 +1,53 @@ +using System.ComponentModel.DataAnnotations; +using static Backend_Teamwork.src.DTO.OrderDetailDTO; +using static Backend_Teamwork.src.DTO.UserDTO; + +namespace Backend_Teamwork.src.DTO +{ + public class OrderDTO + { + // DTO for creating a new order + public class OrderCreateDto + { + [ + Required(ErrorMessage = "Address shouldn't be null"), + MinLength(10, ErrorMessage = "Address should be at at least 10 characters"), + MaxLength(30, ErrorMessage = "Address shouldn't be more than 30 characters") + ] + public string ShippingAddress { get; set; } + public DateTime? CreatedAt { get; set; } = DateTime.Now; + + [Required(ErrorMessage = "Order details shouldn't be null")] + public List OrderDetails { get; set; } + } + + // DTO for reading order data + public class OrderReadDto + { + public Guid Id { get; set; } + public decimal TotalAmount { get; set; } + public string? ShippingAddress { get; set; } + public DateTime? CreatedAt { get; set; } + public UserReadDto User { get; set; } + public List OrderDetails { get; set; } + } + + // DTO for updating an existing order + public class OrderUpdateDto + { + [Range( + 1.0, + double.MaxValue, + ErrorMessage = "Total amount should be greater than zero." + )] + public decimal TotalAmount { get; set; } + + [ + Required(ErrorMessage = "Address shouldn't be null"), + MinLength(10, ErrorMessage = "Address should be at at least 10 characters"), + MaxLength(30, ErrorMessage = "Address shouldn't be more than 30 characters") + ] + public string? ShippingAddress { get; set; } + } + } +} diff --git a/src/DTO/OrderDetailDTO.cs b/src/DTO/OrderDetailDTO.cs new file mode 100644 index 0000000000000000000000000000000000000000..67a95451d8ffa9f05691fe99b5d8c4ac5b062c9d --- /dev/null +++ b/src/DTO/OrderDetailDTO.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using static Backend_Teamwork.src.DTO.ArtworkDTO; + +namespace Backend_Teamwork.src.DTO +{ + public class OrderDetailDTO + { + public class OrderDetailCreateDto + { + [Required(ErrorMessage = "Artwork Id shouldn't be null")] + public Guid ArtworkId { get; set; } + + [Range(1, int.MaxValue, ErrorMessage = "Quantity should be greater than zero.")] + public int Quantity { get; set; } + } + + public class OrderDetailReadDto + { + public Guid Id { get; set; } + public int Quantity { get; set; } + public ArtworkReadDto? Artwork { get; set; } + } + + public class OrderDetailUpdateDto + { + [Range(1, int.MaxValue, ErrorMessage = "Quantity should be greater than zero.")] + public int Quantity { get; set; } + } + } +} diff --git a/src/DTO/PaymentDTO.cs b/src/DTO/PaymentDTO.cs new file mode 100644 index 0000000000000000000000000000000000000000..20917cbfa759d474fdc7b6c5d27ec9bab1a6c499 --- /dev/null +++ b/src/DTO/PaymentDTO.cs @@ -0,0 +1,47 @@ +using System.ComponentModel.DataAnnotations; +using Backend_Teamwork.src.Entities; + +namespace Backend_Teamwork.src.DTO +{ + public class PaymentDTO + { + public class PaymentCreateDTO + { + [ + Required(ErrorMessage = "Payment method shouldn't be null"), + MinLength(10, ErrorMessage = "Payment method should be at at least 10 characters"), + MaxLength(30, ErrorMessage = "Payment method shouldn't be more than 30 characters") + ] + public string PaymentMethod { get; set; } + + [Range(1.0, double.MaxValue, ErrorMessage = "Price should be greater than zero.")] + public decimal Amount { get; set; } + public DateTime? CreatedAt { get; set; } = DateTime.Now; + public Guid? OrderId { get; set; } = Guid.Empty; + public Guid? BookingId { get; set; } = Guid.Empty; + } + + public class PaymentReadDTO + { + public Guid Id { get; set; } + public string PaymentMethod { get; set; } + public decimal Amount { get; set; } + public DateTime CreatedAt { get; set; } + public Order? Order { get; set; } + public Booking? Booking { get; set; } + } + + public class PaymentUpdateDTO + { + [ + Required(ErrorMessage = "Payment method shouldn't be null"), + MinLength(10, ErrorMessage = "Payment method should be at at least 10 characters"), + MaxLength(30, ErrorMessage = "Payment method shouldn't be more than 30 characters") + ] + public string PaymentMethod { get; set; } + + [Range(1.0, double.MaxValue, ErrorMessage = "Amount should be greater than zero.")] + public decimal Amount { get; set; } + } + } +} diff --git a/src/DTO/UserDTO.cs b/src/DTO/UserDTO.cs new file mode 100644 index 0000000000000000000000000000000000000000..92885ab89e525dfc93c9e7453d5807abc33855c6 --- /dev/null +++ b/src/DTO/UserDTO.cs @@ -0,0 +1,98 @@ +using System.ComponentModel.DataAnnotations; +using Backend_Teamwork.src.Utils; +using static Backend_Teamwork.src.Entities.User; + +namespace Backend_Teamwork.src.DTO +{ + public class UserDTO + { + // DTO for creating a new User (including Artist) + public class UserCreateDto + { + [ + Required(ErrorMessage = "Name shouldn't be null"), + MinLength(2, ErrorMessage = "Name should be at at least 2 characters"), + MaxLength(10, ErrorMessage = "Name shouldn't be more than 10 characters") + ] + public string Name { get; set; } + + [ + Required(ErrorMessage = "Phone number shouldn't be null"), + RegularExpression( + @"^\+966[5][0-9]{8}$", + ErrorMessage = "Phone number should be a valid Saudi phone number" + ) + ] + public string PhoneNumber { get; set; } + + [ + Required(ErrorMessage = "Email shouldn't be null"), + EmailAddress(ErrorMessage = "Email should be with right format: @gmail.com") + ] + public string Email { get; set; } + + [ + Required(ErrorMessage = "Password shouldn't be null"), + MinLength(8, ErrorMessage = "Password should be at at least 8 characters") + ] + public string Password { get; set; } + + [Required(ErrorMessage = "Role shouldn't be null")] + public UserRole Role { get; set; } = UserRole.Customer; // Default to Customer + + // Artist-specific properties (optional) + public string? Description { get; set; } // Nullable, only for Artists + } + + public class UserSigninDto + { + [ + Required(ErrorMessage = "Email shouldn't be null"), + EmailAddress(ErrorMessage = "Email should be with right format: @gmail.com") + ] + public string Email { get; set; } + + [Required(ErrorMessage = "Password shouldn't be null")] + public string Password { get; set; } + } + + // DTO for reading User data (including Artist) + public class UserReadDto + { + public Guid Id { get; set; } + public string? Name { get; set; } + public string? PhoneNumber { get; set; } + public string? Email { get; set; } + public UserRole Role { get; set; } + + // Artist-specific properties (optional) + public string? Description { get; set; } // Nullable, only for Artists + } + + // DTO for updating an existing User (including Artist) + [AtLeastOneRequired(ErrorMessage = "At least one property must be updated.")] + public class UserUpdateDto + { + [ + MinLength(2, ErrorMessage = "Name should be at at least 2 characters"), + MaxLength(10, ErrorMessage = "Name shouldn't be more than 10 characters") + ] + public string? Name { get; set; } + + [RegularExpression( + @"^\+966[5][0-9]{8}$", + ErrorMessage = "Phone number should be a valid Saudi phone number" + )] + public string? PhoneNumber { get; set; } + + [EmailAddress(ErrorMessage = "Email should be with right format: @gmail.com")] + public string? Email { get; set; } + + [MinLength(8, ErrorMessage = "Password should be at at least 8 characters")] + public string? Password { get; set; } + + [MinLength(2, ErrorMessage = "Description should be at at least 2 characters")] + public string? Description { get; set; } + } + } +} diff --git a/src/DTO/WorkshopDTO.cs b/src/DTO/WorkshopDTO.cs new file mode 100644 index 0000000000000000000000000000000000000000..68afb1776d613e88146d1dfac30125de2f85ddfe --- /dev/null +++ b/src/DTO/WorkshopDTO.cs @@ -0,0 +1,102 @@ +using System.ComponentModel.DataAnnotations; +using Backend_Teamwork.src.Entities; + +namespace Backend_Teamwork.src.DTO +{ + public class WorkshopDTO + { + public class WorkshopCreateDTO + { + [ + Required(ErrorMessage = "Name shouldn't be null"), + MinLength(10, ErrorMessage = "Name should be at at least 10 characters"), + MaxLength(30, ErrorMessage = "Name shouldn't be more than 30 characters") + ] + public string Name { get; set; } + + [ + Required(ErrorMessage = "Location shouldn't be null"), + MinLength(10, ErrorMessage = "Location should be at at least 10 characters"), + MaxLength(30, ErrorMessage = "Location shouldn't be more than 30 characters") + ] + public string Location { get; set; } + + [ + Required(ErrorMessage = "Description shouldn't be null"), + MinLength(30, ErrorMessage = "Description should be at at least 30 characters"), + MaxLength(200, ErrorMessage = "Description shouldn't be more than 200 characters") + ] + public string Description { get; set; } + + [Required(ErrorMessage = "StartTime shouldn't be null")] + public DateTime StartTime { get; set; } + + [Required(ErrorMessage = "EndTime shouldn't be null")] + public DateTime EndTime { get; set; } + + [Range(1.0, double.MaxValue, ErrorMessage = "Price should be greater than zero.")] + public decimal Price { get; set; } + + [Range(1, int.MaxValue, ErrorMessage = "Capacity should be greater than zero.")] + public int Capacity { get; set; } + + [Required(ErrorMessage = "Availability shouldn't be null")] + public bool Availability { get; set; } + public DateTime? CreatedAt { get; set; } = DateTime.Now; + } + + public class WorkshopReadDTO + { + public Guid Id { get; set; } + public string? Name { get; set; } + public string? Location { get; set; } + public string? Description { get; set; } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public decimal Price { get; set; } + public int Capacity { get; set; } + public bool Availability { get; set; } + public DateTime CreatedAt { get; set; } + public User User { get; set; } + } + + public class WorkshopUpdateDTO + { + [ + Required(ErrorMessage = "Name shouldn't be null"), + MinLength(10, ErrorMessage = "Name should be at at least 10 characters"), + MaxLength(30, ErrorMessage = "Name shouldn't be more than 30 characters") + ] + public string Name { get; set; } + + [ + Required(ErrorMessage = "Location shouldn't be null"), + MinLength(10, ErrorMessage = "Location should be at at least 10 characters"), + MaxLength(30, ErrorMessage = "Location shouldn't be more than 30 characters") + ] + public string Location { get; set; } + + [ + Required(ErrorMessage = "Description shouldn't be null"), + MinLength(30, ErrorMessage = "Description should be at at least 30 characters"), + MaxLength(200, ErrorMessage = "Description shouldn't be more than 200 characters") + ] + public string Description { get; set; } + + [Required(ErrorMessage = "StartTime shouldn't be null")] + public DateTime StartTime { get; set; } + + [Required(ErrorMessage = "EndTime shouldn't be null")] + public DateTime EndTime { get; set; } + + [Required(ErrorMessage = "Availability shouldn't be null")] + public bool Availability { get; set; } + + [Range(1.0, double.MaxValue, ErrorMessage = "Price should be greater than zero.")] + public decimal Price { get; set; } + + [Range(1, int.MaxValue, ErrorMessage = "Capacity should be greater than zero.")] + public int Capacity { get; set; } + } + } +} diff --git a/src/Database/DatabaseContext.cs b/src/Database/DatabaseContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..cb48a498b61b50adf1c762a3dee332f267a6a1a9 --- /dev/null +++ b/src/Database/DatabaseContext.cs @@ -0,0 +1,50 @@ +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Utils; +using Microsoft.EntityFrameworkCore; +using static Backend_Teamwork.src.Entities.User; + +namespace Backend_Teamwork.src.Database +{ + public class DatabaseContext : DbContext + { + public DbSet Category { get; set; } + public DbSet Artwork { get; set; } + public DbSet Order { get; set; } + public DbSet OrderDetail { get; set; } + public DbSet Payment { get; set; } + public DbSet Workshop { get; set; } + public DbSet User { get; set; } + public DbSet Booking { get; set; } + + public DatabaseContext(DbContextOptions option) + : base(option) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasPostgresEnum(); + modelBuilder.HasPostgresEnum(); + + modelBuilder.Entity().HasIndex(x => x.PhoneNumber).IsUnique(); + modelBuilder.Entity().HasIndex(x => x.Email).IsUnique(); + + modelBuilder + .Entity() + .HasData( + new User + { + Id = Guid.NewGuid(), + Name = "Abeer", + PhoneNumber = "0563034770", + Email = "abeer@gmail.com", + Password = PasswordUtils.HashPassword( + "123", + out string hashedPassword, + out byte[] salt + ), + Role = UserRole.Admin, + Salt = salt, + } + ); + } + } +} diff --git a/src/Entities/Artwork.cs b/src/Entities/Artwork.cs new file mode 100644 index 0000000000000000000000000000000000000000..6cbac32262553ae84d3feda0f4cef2592439a4cb --- /dev/null +++ b/src/Entities/Artwork.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; + +namespace Backend_Teamwork.src.Entities +{ + public class Artwork + { + public Guid Id { get; set; } + + [ + Required(ErrorMessage = "Title shouldn't be null"), + MinLength(6, ErrorMessage = "Title should be at at least 6 characters"), + MaxLength(30, ErrorMessage = "Title shouldn't be more than 30 characters") + ] + public required string Title { get; set; } + + [ + Required(ErrorMessage = "Description shouldn't be null"), + MinLength(30, ErrorMessage = "Description should be at at least 30 characters"), + MaxLength(200, ErrorMessage = "Description shouldn't be more than 200 characters") + ] + public required string Description { get; set; } + public required string ImagUrl { get; set; } + + [Range(1, int.MaxValue, ErrorMessage = "Quantity should be greater than zero.")] + public int Quantity { get; set; } + + [Range(1.0, double.MaxValue, ErrorMessage = "Price should be greater than zero.")] + public decimal Price { get; set; } + public DateTime CreatedAt { get; set; } + + [Required(ErrorMessage = "User Id shouldn't be null")] + public Guid UserId { get; set; } + public User User { get; set; } = null!; + + [Required(ErrorMessage = "Category Id shouldn't be null")] + public Guid CategoryId { get; set; } + public Category Category { get; set; } = null!; + + // OrderDetails + public List? OrderDetails { get; set; } + } +} diff --git a/src/Entities/Booking.cs b/src/Entities/Booking.cs new file mode 100644 index 0000000000000000000000000000000000000000..563b0faf87e9288aa78cbd8492392f0b3f0f1ef8 --- /dev/null +++ b/src/Entities/Booking.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Backend_Teamwork.src.Entities +{ + public class Booking + { + public Guid Id { get; set; } + + [Required(ErrorMessage = "Status shouldn't be null")] + public Status Status { get; set; } + public DateTime CreatedAt { get; set; } + + [Required(ErrorMessage = "Workshop Id shouldn't be null")] + public Guid WorkshopId { get; set; } + public Workshop Workshop { get; set; } = null!; + + [Required(ErrorMessage = "User Id shouldn't be null")] + public Guid UserId { get; set; } + public User User { get; set; } = null!; + public Payment? Payment { get; set; } + } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum Status + { + Pending, + Confirmed, + Canceled, + Rejected, + } +} diff --git a/src/Entities/Category.cs b/src/Entities/Category.cs new file mode 100644 index 0000000000000000000000000000000000000000..757b6fea2bbc8f1ee3e422f53eb511865d23ee0c --- /dev/null +++ b/src/Entities/Category.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + + +namespace Backend_Teamwork.src.Entities +{ + public class Category + { + public Guid Id { set; get; } + [ + Required(ErrorMessage = "Name shouldn't be null"), + MinLength(2, ErrorMessage = "Name should be at at least 2 characters"), + MaxLength(10, ErrorMessage = "Name shouldn't be more than 10 characters") + ] + public string Name { set; get; } + } +} diff --git a/src/Entities/Order.cs b/src/Entities/Order.cs new file mode 100644 index 0000000000000000000000000000000000000000..c13657d35d44658f42f07274d7b6d0eae89cdbb4 --- /dev/null +++ b/src/Entities/Order.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; + +namespace Backend_Teamwork.src.Entities +{ + public class Order + { + public Guid Id { get; set; } + + [Range(1.0, double.MaxValue, ErrorMessage = "Total amount should be greater than zero.")] + public decimal TotalAmount { get; set; } + + [ + Required(ErrorMessage = "Address shouldn't be null"), + MinLength(10, ErrorMessage = "Address should be at at least 10 characters"), + MaxLength(30, ErrorMessage = "Address shouldn't be more than 30 characters") + ] + public string ShippingAddress { get; set; } + public DateTime CreatedAt { get; set; } + + [Required(ErrorMessage = "User Id shouldn't be null")] + public Guid UserId { get; set; } + public User User { get; set; } = null!; + + [Required(ErrorMessage = "Order details shouldn't be null")] + public List OrderDetails { get; set; } + public Payment? Payment { get; set; } + } +} diff --git a/src/Entities/OrderDetails.cs b/src/Entities/OrderDetails.cs new file mode 100644 index 0000000000000000000000000000000000000000..44de2800756aff9d30a828812cdb2752d7b3f090 --- /dev/null +++ b/src/Entities/OrderDetails.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace Backend_Teamwork.src.Entities +{ + public class OrderDetails + { + public Guid Id { get; set; } + public Artwork Artwork { get; set; } = null!; + + [Required(ErrorMessage = "Artwork Id shouldn't be null")] + public Guid ArtworkId { get; set; } + public Order Order { get; set; } = null!; + + [Required(ErrorMessage = "Order Id shouldn't be null")] + public Guid OrderId { get; set; } + + [Range(1, int.MaxValue, ErrorMessage = "Quantity should be greater than zero.")] + public int Quantity { get; set; } + } +} diff --git a/src/Entities/Payment.cs b/src/Entities/Payment.cs new file mode 100644 index 0000000000000000000000000000000000000000..41469280b55621eb88c01ed049596215f0a142b5 --- /dev/null +++ b/src/Entities/Payment.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; + +namespace Backend_Teamwork.src.Entities +{ + public class Payment + { + public Guid Id { get; set; } + + [ + Required(ErrorMessage = "Payment method shouldn't be null"), + MinLength(10, ErrorMessage = "Payment method should be at at least 10 characters"), + MaxLength(30, ErrorMessage = "Payment method shouldn't be more than 30 characters") + ] + public string PaymentMethod { get; set; } + + [Range(1.0, double.MaxValue, ErrorMessage = "Amount should be greater than zero.")] + public decimal Amount { get; set; } + public DateTime CreatedAt { get; set; } + public Guid? OrderId { get; set; } + public Order? Order { get; set; } + public Guid? BookingId { get; set; } + public Booking? Booking { get; set; } + } +} diff --git a/src/Entities/User.cs b/src/Entities/User.cs new file mode 100644 index 0000000000000000000000000000000000000000..551e309f1257fe0373337344b8a2641f1470bac9 --- /dev/null +++ b/src/Entities/User.cs @@ -0,0 +1,54 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + + +namespace Backend_Teamwork.src.Entities +{ + public class User + { + public Guid Id { get; set; } + + [ + Required(ErrorMessage = "Name shouldn't be null"), + MinLength(2, ErrorMessage = "Name should be at at least 2 characters"), + MaxLength(10, ErrorMessage = "Name shouldn't be more than 10 characters") + ] + public string Name { get; set; } + + [ + Required(ErrorMessage = "Phone number shouldn't be null"), + RegularExpression( + @"^\+966[5][0-9]{8}$", + ErrorMessage = "Phone number should be a valid Saudi phone number" + ) + ] + public string PhoneNumber { get; set; } + + [ + Required(ErrorMessage = "Email shouldn't be null"), + EmailAddress(ErrorMessage = "Email should be with right format: @gmail.com") + ] + public string Email { get; set; } + + [ + Required(ErrorMessage = "Password shouldn't be null."), + MinLength(8, ErrorMessage = "Password should be at at least 8 characters") + ] + public string Password { get; set; } + public string? Description { set; get; } + + [Required(ErrorMessage = "Salt shouldn't be null")] + public byte[]? Salt { get; set; } + + [Required(ErrorMessage = "Role shouldn't be null")] + public UserRole Role { get; set; } = UserRole.Customer; + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum UserRole + { + Admin, + Customer, + Artist, + } + } +} diff --git a/src/Entities/Workshop.cs b/src/Entities/Workshop.cs new file mode 100644 index 0000000000000000000000000000000000000000..86ff50a8227d3b27f62d17ac8f7fd27c083a81aa --- /dev/null +++ b/src/Entities/Workshop.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations; + +namespace Backend_Teamwork.src.Entities +{ + public class Workshop + { + public Guid Id { get; set; } + + [ + Required(ErrorMessage = "Name shouldn't be null"), + MinLength(10, ErrorMessage = "Name should be at at least 10 characters"), + MaxLength(30, ErrorMessage = "Name shouldn't be more than 30 characters") + ] + public string Name { get; set; } + + [ + Required(ErrorMessage = "Location shouldn't be null"), + MinLength(10, ErrorMessage = "Location should be at at least 10 characters"), + MaxLength(30, ErrorMessage = "Location shouldn't be more than 30 characters") + ] + public string Location { get; set; } + + [ + Required(ErrorMessage = "Description shouldn't be null"), + MinLength(30, ErrorMessage = "Description should be at at least 30 characters"), + MaxLength(200, ErrorMessage = "Description shouldn't be more than 200 characters") + ] + public string Description { get; set; } + + [Required(ErrorMessage = "StartTime shouldn't be null")] + public DateTime StartTime { get; set; } + + [Required(ErrorMessage = "EndTime shouldn't be null")] + public DateTime EndTime { get; set; } + + [Range(1.0, double.MaxValue, ErrorMessage = "Price should be greater than zero.")] + public decimal Price { get; set; } + + [Range(1, int.MaxValue, ErrorMessage = "Capacity should be greater than zero.")] + public int Capacity { get; set; } + + [Required(ErrorMessage = "Availability shouldn't be null")] + public bool Availability { get; set; } + public DateTime? CreatedAt { get; set; } = DateTime.Now; + + [Required(ErrorMessage = "User Id shouldn't be null")] + public Guid UserId { get; set; } + public User User { get; set; } = null!; + } +} diff --git a/src/Middleware/ErrorHandlerMiddleware.cs b/src/Middleware/ErrorHandlerMiddleware.cs new file mode 100644 index 0000000000000000000000000000000000000000..1970e2abdb04df7b73ef66110b697d73e7e4abc1 --- /dev/null +++ b/src/Middleware/ErrorHandlerMiddleware.cs @@ -0,0 +1,21 @@ +using Backend_Teamwork.src.Utils; + +namespace Backend_Teamwork.src.Middleware +{ + public class ErrorHandlerMiddleware + { + private readonly RequestDelegate _next; + public ErrorHandlerMiddleware(RequestDelegate next){ + _next = next; + } + public async Task InvokeAsync(HttpContext context){ + try{ + await _next(context); + }catch(CustomException ex){ + context.Response.StatusCode = ex.StatusCode; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(ex.Message); + } + } + } +} \ No newline at end of file diff --git a/src/Program.cs b/src/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..5566461f52465aee159c284736ec620773e1ac8d --- /dev/null +++ b/src/Program.cs @@ -0,0 +1,152 @@ +using System.Text; +using System.Text.Json.Serialization; +using Backend_Teamwork.src.Database; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Middleware; +using Backend_Teamwork.src.Repository; +using Backend_Teamwork.src.Services.artwork; +using Backend_Teamwork.src.Services.booking; +using Backend_Teamwork.src.Services.category; +using Backend_Teamwork.src.Services.order; +using Backend_Teamwork.src.Services.user; +using Backend_Teamwork.src.Services.workshop; +using Backend_Teamwork.src.Utils; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Npgsql; +using static Backend_Teamwork.src.Entities.User; + +var builder = WebApplication.CreateBuilder(args); + +//connect to database +var dataSourceBuilder = new NpgsqlDataSourceBuilder( + builder.Configuration.GetConnectionString("Local") +); +dataSourceBuilder.MapEnum(); +dataSourceBuilder.MapEnum(); + +//add database connection +builder.Services.AddDbContext(options => +{ + options.UseNpgsql(dataSourceBuilder.Build()); +}); + +//add auto-mapper +builder.Services.AddAutoMapper(typeof(MapperProfile).Assembly); + +//add DI services +builder.Services.AddScoped().AddScoped(); +builder.Services.AddScoped().AddScoped(); +builder.Services.AddScoped().AddScoped(); +builder.Services.AddScoped().AddScoped(); +builder.Services.AddScoped().AddScoped(); +builder.Services.AddScoped().AddScoped(); + +//builder.Services.AddScoped().AddScoped(); + + +//add logic for authentication +builder + .Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(Options => + { + Options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + ValidAudience = builder.Configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]) + ), + }; + }); + +//add logic for athorization +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin")); + options.AddPolicy("CustomerOnly", policy => policy.RequireRole("Customer")); +}); + +// *** add CORS settings *** +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowAll", + policyBuilder => policyBuilder.AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod()); +}); +// builder.Services.AddCors(options => +// { +// options.AddPolicy("AllowSpecificOrigin", +// builder => builder.WithOrigins("http://localhost:5173") +// .AllowAnyHeader() +// .AllowAnyMethod()); +// }); + + +//add controllers +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder + .Services.AddControllers() + .AddJsonOptions(x => x.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles); + +//add swagger +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); +app.UseRouting(); +app.MapGet("/", () => "Server is running"); + +//Convert to Timestamp format +AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); + +// + +//test database connection +using (var scope = app.Services.CreateScope()) +{ + var dbContext = scope.ServiceProvider.GetRequiredService(); + try + { + if (dbContext.Database.CanConnect()) + { + Console.WriteLine("Database is connected"); + dbContext.Database.Migrate(); + } + else + { + Console.WriteLine("Unable to connect to the database."); + } + } + catch (Exception ex) + { + Console.WriteLine($"Database connection failed: {ex.Message}"); + } +} +app.UseHttpsRedirection(); + +//use middleware +app.UseMiddleware(); +app.UseAuthentication(); +app.UseAuthorization(); + +//use controllers +app.MapControllers(); + +//use swagger +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} +app.Run(); diff --git a/src/Repository/ArtworkRepository.cs b/src/Repository/ArtworkRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..a28a0738227fe76cf84044134bcfcb6ccc6ce2dc --- /dev/null +++ b/src/Repository/ArtworkRepository.cs @@ -0,0 +1,101 @@ +using Backend_Teamwork.src.Database; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Utils; +using Microsoft.EntityFrameworkCore; + +namespace Backend_Teamwork.src.Repository +{ + public class ArtworkRepository + { + private readonly DbSet _artwork; + private readonly DatabaseContext _databaseContext; // for dependency injection + + // Dependency Injection + public ArtworkRepository(DatabaseContext databaseContext) + { + _databaseContext = databaseContext; + // initialize artwork table in the database + _artwork = databaseContext.Set(); + } + + // Methods + // create artwork + public async Task CreateOneAsync(Artwork newArtwork) + { + await _artwork.AddAsync(newArtwork); + await _databaseContext.SaveChangesAsync(); + return await GetByIdAsync(newArtwork.Id); + } + + // get all artworks + public async Task> GetAllAsync(PaginationOptions paginationOptions) + { + var artworkSearch = _artwork.Where(a => + a.Title.ToLower().Contains(paginationOptions.Search.ToLower()) + ); + + artworkSearch = artworkSearch.Where(a => + a.Price >= paginationOptions.LowPrice && a.Price <= paginationOptions.HighPrice + ); + + artworkSearch = artworkSearch + .Skip((paginationOptions.PageNumber - 1) * paginationOptions.PageSize) + .Take(paginationOptions.PageSize); + + artworkSearch = paginationOptions.SortOrder switch + { + "name_desc" => artworkSearch.OrderByDescending(a => a.Title), + "date" => artworkSearch.OrderBy(a => a.CreatedAt), + "date_desc" => artworkSearch.OrderByDescending(a => a.CreatedAt), + "price" => artworkSearch.OrderBy(a => a.Price), + "price_desc" => artworkSearch.OrderByDescending(a => a.Price), + _ => artworkSearch.OrderBy(a => a.Title), + }; + + return await artworkSearch.Include(o => o.Category).Include(o => o.User).ToListAsync(); + } + + // get artwork by id + public async Task GetByIdAsync(Guid id) + { + return await _artwork + .Include(a => a.Category) + .Include(a => a.User) + .FirstOrDefaultAsync(a => a.Id == id); + } + + // get artworks by artist id + public async Task> GetByArtistIdAsync(Guid id) + { + return await _artwork.Include(a => a.Category).Where(a => a.UserId == id).ToListAsync(); + } + + // delete artwork + public async Task DeleteOneAsync(Artwork artwork) + { + _artwork.Remove(artwork); + await _databaseContext.SaveChangesAsync(); + return true; + } + + // update artwork + public async Task UpdateOneAsync(Artwork updateArtwork) + { + _artwork.Update(updateArtwork); + await _databaseContext.SaveChangesAsync(); + return await GetByIdAsync(updateArtwork.Id); + } + + // Count total artworks + public async Task CountAsync() + { + return await _artwork.CountAsync(); + } + + // Count artworks by artist + public async Task CountByArtistAsync(Guid artistId) + { + return await _artwork.CountAsync(a => a.UserId == artistId); + } + } +} diff --git a/src/Repository/BookingRepository.cs b/src/Repository/BookingRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..a466423f53ab94acea68b2b1e1888ae3d724c9c2 --- /dev/null +++ b/src/Repository/BookingRepository.cs @@ -0,0 +1,121 @@ +using Backend_Teamwork.src.Database; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Utils; +using Microsoft.EntityFrameworkCore; + +namespace Backend_Teamwork.src.Repository +{ + public class BookingRepository + { + private readonly DbSet _booking; + private readonly DatabaseContext _databaseContext; + + public BookingRepository(DatabaseContext databaseContext) + { + _databaseContext = databaseContext; + _booking = databaseContext.Set(); + } + + public async Task> GetAllAsync() + { + return await _booking + .Include(b => b.User) + .Include(b => b.Workshop) + .ThenInclude(w => w.User) + .ToListAsync(); + } + + public async Task GetByIdAsync(Guid id) + { + return await _booking + .Include(b => b.User) + .Include(b => b.Workshop) + .ThenInclude(w => w.User) + .FirstOrDefaultAsync(b => b.Id == id); + } + + public async Task> GetByUserIdAsync(Guid userId) + { + return await _booking + .Include(b => b.User) + .Include(b => b.Workshop) + .ThenInclude(w => w.User) + .Where(b => b.UserId == userId) + .ToListAsync(); + } + + public async Task> GetByStatusAsync(string status) + { + return await _booking + .Include(b => b.User) + .Include(b => b.Workshop) + .ThenInclude(w => w.User) + .Where(b => b.Status.ToString().ToLower() == status.ToLower()) + .ToListAsync(); + } + + public async Task> GetByUserIdAndStatusAsync(Guid userId, string status) + { + return await _booking + .Include(b => b.User) + .Include(b => b.Workshop) + .ThenInclude(w => w.User) + .Where(b => b.Status.ToString() == status.ToString() && b.UserId == userId) + .ToListAsync(); + } + + public async Task> GetByWorkshopIdAndStatusAsync( + Guid workshopId, + Status status + ) + { + return await _booking + .Where(b => b.WorkshopId == workshopId && b.Status == status) + .ToListAsync(); + } + + public async Task GetByUserIdAndWorkshopIdAsync(Guid userId, Guid workshopId) + { + return await _booking.AnyAsync(b => b.UserId == userId && b.WorkshopId == workshopId); + } + + public async Task> GetWithPaginationAsync(PaginationOptions paginationOptions) + { + return await _booking + .Include(b => b.User) + .Include(b => b.Workshop) + .ThenInclude(w => w.User) + .Skip((paginationOptions.PageNumber - 1) * paginationOptions.PageSize) + .Take(paginationOptions.PageSize) + .ToListAsync(); + } + + public async Task> GetByUserIdWithPaginationAsync( + PaginationOptions paginationOptions + ) + { + var bookings = _booking.Where(b => b.UserId.ToString() == paginationOptions.Search); + return await bookings + .Include(b => b.User) + .Include(b => b.Workshop) + .ThenInclude(w => w.User) + .Skip((paginationOptions.PageNumber - 1) * paginationOptions.PageSize) + .Take(paginationOptions.PageSize) + .ToListAsync(); + } + + public async Task CreateAsync(Booking booking) + { + await _booking.AddAsync(booking); + await _databaseContext.SaveChangesAsync(); + return await GetByIdAsync(booking.Id); + } + + public async Task UpdateAsync(Booking booking) + { + _booking.Update(booking); + await _databaseContext.SaveChangesAsync(); + return await GetByIdAsync(booking.Id); + } + } +} diff --git a/src/Repository/CategoryRepository.cs b/src/Repository/CategoryRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..320ca16359c6a5ee2fcf000586601913afe8d0f1 --- /dev/null +++ b/src/Repository/CategoryRepository.cs @@ -0,0 +1,68 @@ +using Backend_Teamwork.src.Database; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Utils; +using Microsoft.EntityFrameworkCore; + +namespace Backend_Teamwork.src.Repository +{ + public class CategoryRepository + { + private readonly DbSet _category; + private readonly DatabaseContext _databaseContext; + + public CategoryRepository(DatabaseContext databaseContext) + { + _databaseContext = databaseContext; + _category = databaseContext.Set(); + } + + public async Task> GetAllAsync() + { + return await _category.ToListAsync(); + } + + public async Task GetByIdAsync(Guid id) + { + return await _category.FirstOrDefaultAsync(c => c.Id == id); + } + + public async Task GetByNameAsync(string name) + { + return await _category.FirstOrDefaultAsync(c => c.Name.ToLower() == name.ToLower()); + } + + public async Task> GetWithPaginationAsync(PaginationOptions paginationOptions) + { + return await _category + .Skip((paginationOptions.PageNumber - 1) * paginationOptions.PageSize) + .Take(paginationOptions.PageSize) + .OrderBy(c => c.Name) + .ToListAsync(); + } + + public async Task> SortByNameAsync() + { + return await _category.OrderBy(c => c.Name).ToListAsync(); + } + + public async Task CreateAsync(Category category) + { + await _category.AddAsync(category); + await _databaseContext.SaveChangesAsync(); + return category; + } + + public async Task UpdateAsync(Category category) + { + _category.Update(category); + await _databaseContext.SaveChangesAsync(); + return category; + } + + public async Task DeleteAsync(Category category) + { + _category.Remove(category); + await _databaseContext.SaveChangesAsync(); + } + } +} diff --git a/src/Repository/OrderRepository.cs b/src/Repository/OrderRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..77cb0796bce6ccc9b17b4d231de1bae82b62d59c --- /dev/null +++ b/src/Repository/OrderRepository.cs @@ -0,0 +1,105 @@ +using Backend_Teamwork.src.Database; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Utils; +using Microsoft.EntityFrameworkCore; + +namespace Backend_Teamwork.src.Repository +{ + public class OrderRepository + { + private readonly DbSet _order; + private readonly DatabaseContext _databaseContext; + + public OrderRepository(DatabaseContext databaseContext) + { + _databaseContext = databaseContext; + _order = databaseContext.Set(); + } + + public async Task> GetAllAsync() + { + // Include User, OrderDetails, Artwork, and Category + return await _order + .Include(o => o.User) // Include User details + .Include(o => o.OrderDetails) + .ThenInclude(od => od.Artwork) // Include Artwork + .ThenInclude(a => a.Category) // Include Category if it's a related entity + .ToListAsync(); + } + + public async Task> GetOrdersByUserIdAsync(Guid userId) + { + return await _databaseContext + .Order.Include(o => o.User) // Include User + .Include(o => o.OrderDetails) + .ThenInclude(od => od.Artwork) // Include Artwork + .ThenInclude(a => a.Category) // Include Category in Artwork + .Where(order => order.UserId == userId) + .ToListAsync(); + } + + public async Task CreateOneAsync(Order newOrder) + { + await _order.AddAsync(newOrder); + await _databaseContext.SaveChangesAsync(); + return await GetByIdAsync(newOrder.Id); + } + + public async Task GetByIdAsync(Guid id) + { + return await _order + .Include(o => o.User) // Include User + .Include(o => o.OrderDetails) + .ThenInclude(od => od.Artwork) // Include Artwork + .ThenInclude(a => a.Category) // Include Category in Artwork + .FirstOrDefaultAsync(o => o.Id == id); + } + + public async Task DeleteOneAsync(Order Order) + { + if (Order == null) + return false; + _order.Remove(Order); + await _databaseContext.SaveChangesAsync(); + return true; + } + + public async Task UpdateOneAsync(Order updateOrder) + { + if (updateOrder == null) + return false; + _order.Update(updateOrder); + await _databaseContext.SaveChangesAsync(); + return true; + } + + public async Task> GetAllAsync(PaginationOptions paginationOptions) + { + // Query for orders with optional search + var orderQuery = _order + .Include(o => o.OrderDetails) // Include order details + .Include(o => o.User) + .Where(o => + o.ShippingAddress.Contains(paginationOptions.Search) + || o.TotalAmount.ToString().Contains(paginationOptions.Search) + ); + + // Apply pagination + orderQuery = orderQuery + .Skip((paginationOptions.PageNumber - 1) * paginationOptions.PageSize) + .Take(paginationOptions.PageSize); + + // Sorting logic + orderQuery = paginationOptions.SortOrder switch + { + "amount_desc" => orderQuery.OrderByDescending(o => o.TotalAmount), + "amount_asc" => orderQuery.OrderBy(o => o.TotalAmount), + "date_desc" => orderQuery.OrderByDescending(o => o.CreatedAt), + "date_asc" => orderQuery.OrderBy(o => o.CreatedAt), + _ => orderQuery.OrderBy(o => o.ShippingAddress), // Default sorting by ShippingAddress + }; + + return await orderQuery.ToListAsync(); + } + } +} diff --git a/src/Repository/PaymentRepository.cs b/src/Repository/PaymentRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..3ac3d834fed60a610f245b36281f8474e3ff7074 --- /dev/null +++ b/src/Repository/PaymentRepository.cs @@ -0,0 +1,59 @@ +using Backend_Teamwork.src.Database; +using Backend_Teamwork.src.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Backend_Teamwork.src.Repository +{ + public class PaymentRepository + { + private readonly DbSet _payment; + private readonly DatabaseContext _databaseContext; + + public PaymentRepository(DatabaseContext databaseContext) + { + _databaseContext = databaseContext; + _payment = databaseContext.Set(); + } + + // create in database + public async Task CreateOneAsync(Payment newPayment) + { + await _payment.AddAsync(newPayment); + await _databaseContext.SaveChangesAsync(); + return await GetByIdAsync(newPayment.Id); + } + + // get by id + public async Task GetByIdAsync(Guid id) + { + return await _payment + .Include(p => p.Order) + .Include(p => p.Booking) + .FirstOrDefaultAsync(p => p.Id == id); + } + + // delete + public async Task DeleteOneAsync(Payment deletePayment) + { + _payment.Remove(deletePayment); + await _databaseContext.SaveChangesAsync(); + return true; + } + + // update + public async Task UpdateOneAsync(Payment updatePayment) + { + if (updatePayment == null) + return false; + _payment.Update(updatePayment); + await _databaseContext.SaveChangesAsync(); + return true; + } + + // get all + public async Task> GetAllAsync() + { + return await _payment.Include(p => p.Order).Include(p => p.Booking).ToListAsync(); + } + } +} diff --git a/src/Repository/UserRepository.cs b/src/Repository/UserRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..836277c0c992f05167d2b73e16ed0755d64e5c82 --- /dev/null +++ b/src/Repository/UserRepository.cs @@ -0,0 +1,96 @@ +using Backend_Teamwork.src.Database; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Utils; +using Microsoft.EntityFrameworkCore; + +namespace Backend_Teamwork.src.Repository +{ + public class UserRepository + { + private readonly DbSet _user; + private readonly DatabaseContext _databaseContext; + + public UserRepository(DatabaseContext databaseContext) + { + _databaseContext = databaseContext; + _user = databaseContext.Set(); + } + + public async Task> GetAllAsync(PaginationOptions paginationOptions) + { + // Combined search logic with OR for name, email, or phone number + var userQuery = _user.Where(a => + a.Name.ToLower().Contains(paginationOptions.Search.ToLower()) + || a.Email.ToLower().Contains(paginationOptions.Search.ToLower()) + || a.PhoneNumber.Contains(paginationOptions.Search) // if u try this the dont put the number with +, it does not work + // I think I need to add the country code in the front-end part for the phone number search. + ); + + // Apply pagination + userQuery = userQuery + .Skip((paginationOptions.PageNumber - 1) * paginationOptions.PageSize) + .Take(paginationOptions.PageSize); + + // Sorting logic + userQuery = paginationOptions.SortOrder switch + { + "name_desc" => userQuery.OrderByDescending(a => a.Name), + "email_desc" => userQuery.OrderByDescending(a => a.Email), + "email_asc" => userQuery.OrderBy(a => a.Email), + _ => userQuery.OrderBy(a => a.Name), // Default to ascending by name + }; + + return await userQuery.ToListAsync(); + } + + public async Task GetCountAsync() + { + return await _user.CountAsync(); + } + + public async Task CreateOneAsync(User newUser) + { + await _user.AddAsync(newUser); + await _databaseContext.SaveChangesAsync(); + return newUser; + } + + public async Task GetByIdAsync(Guid id) + { + return await _user.FindAsync(id); + } + + public async Task DeleteOneAsync(User User) + { + if (User == null) + return false; + _user.Remove(User); + await _databaseContext.SaveChangesAsync(); + return true; + } + + public async Task UpdateOneAsync(User updateUser) + { + if (updateUser == null) + return false; + _user.Update(updateUser); + await _databaseContext.SaveChangesAsync(); + return true; + } + + public async Task GetByEmailAsync(string email) + { + return await _user.FirstOrDefaultAsync(c => c.Email == email); + } + + public async Task GetByPhoneNumberAsync(string phoneNumber) + { + return await _user.FirstOrDefaultAsync(c => c.PhoneNumber == phoneNumber); + } + + public async Task GetByNameAsync(string name) + { + return await _user.FirstOrDefaultAsync(c => c.Name == name); + } + } +} diff --git a/src/Repository/WorkshopRepository.cs b/src/Repository/WorkshopRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..7036333a1eca6ecbbea67c5f5c916251c0ce21ff --- /dev/null +++ b/src/Repository/WorkshopRepository.cs @@ -0,0 +1,87 @@ +using Backend_Teamwork.src.Database; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Utils; +using Microsoft.EntityFrameworkCore; + +namespace Backend_Teamwork.src.Repository +{ + public class WorkshopRepository + { + private readonly DbSet _workshops; + private readonly DatabaseContext _databaseContext; + + public WorkshopRepository(DatabaseContext databaseContext) + { + _databaseContext = databaseContext; + _workshops = databaseContext.Set(); + } + + // create in database + public async Task CreateOneAsync(Workshop newWorkshop) + { + await _workshops.AddAsync(newWorkshop); + await _databaseContext.SaveChangesAsync(); + return await GetByIdAsync(newWorkshop.Id); + } + + // get by id + public async Task GetByIdAsync(Guid id) + { + return await _workshops.Include(o => o.User).FirstOrDefaultAsync(o => o.Id == id); + } + + // delete + public async Task DeleteOneAsync(Workshop deleteWorkshop) + { + _workshops.Remove(deleteWorkshop); + await _databaseContext.SaveChangesAsync(); + return true; + } + + // update + public async Task UpdateOneAsync(Workshop updateWorkshop) + { + if (updateWorkshop == null) + return false; + _workshops.Update(updateWorkshop); + await _databaseContext.SaveChangesAsync(); + return true; + } + + // get all + public async Task> GetAllAsync() + { + return await _workshops.Include(o => o.User).ToListAsync(); + } + + public async Task> GetAllAsync(PaginationOptions paginationOptions) + { + // Combined search logic with OR for name, email, or phone number + var userQuery = _workshops.Where(a => + a.Name.ToLower().Contains(paginationOptions.Search.ToLower()) + || a.Location.ToLower().Contains(paginationOptions.Search.ToLower()) + ); + + // Apply pagination + userQuery = userQuery + .Skip((paginationOptions.PageNumber - 1) * paginationOptions.PageSize) + .Take(paginationOptions.PageSize); + + // Apply sorting logic + userQuery = paginationOptions.SortOrder switch + { + "name_desc" => userQuery.OrderByDescending(a => a.Name), + "location_asc" => userQuery.OrderBy(a => a.Location), + "location_desc" => userQuery.OrderByDescending(a => a.Location), + "price_desc" => userQuery.OrderByDescending(a => a.Price), + "price_asc" => userQuery.OrderBy(a => a.Price), + "date_desc" => userQuery.OrderByDescending(a => a.CreatedAt), + "date_asc" => userQuery.OrderBy(a => a.CreatedAt), + "capacity_desc" => userQuery.OrderByDescending(a => a.Capacity), + _ => userQuery.OrderBy(a => a.Name), // Default to ascending by name + }; + + return await userQuery.ToListAsync(); + } + } +} diff --git a/src/Services/artwork/ArtworkService.cs b/src/Services/artwork/ArtworkService.cs new file mode 100644 index 0000000000000000000000000000000000000000..1f83276e481cac6b04025c940f3a00d40c9325fe --- /dev/null +++ b/src/Services/artwork/ArtworkService.cs @@ -0,0 +1,122 @@ +using AutoMapper; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Repository; +using Backend_Teamwork.src.Utils; +using static Backend_Teamwork.src.DTO.ArtworkDTO; +using static Backend_Teamwork.src.Entities.User; + +namespace Backend_Teamwork.src.Services.artwork +{ + public class ArtworkService : IArtworkService + { + private readonly ArtworkRepository _artworkRepo; + private readonly UserRepository _userRepo; + private readonly CategoryRepository _categoryRepo; + private readonly IMapper _mapper; + + public ArtworkService( + ArtworkRepository artworkRepo, + UserRepository userRepo, + CategoryRepository categoryRepo, + IMapper mapper + ) + { + _artworkRepo = artworkRepo; + _userRepo = userRepo; + _categoryRepo = categoryRepo; + _mapper = mapper; + } + + public async Task CreateOneAsync(Guid artistId, ArtworkCreateDto createDto) + { + var foundCategory = await _categoryRepo.GetByIdAsync(createDto.CategoryId); + if (foundCategory == null) + { + throw CustomException.NotFound($"Category with id: {createDto.CategoryId} not found"); + } + var artwork = _mapper.Map(createDto); + artwork.UserId = artistId; + var createdArtwork = await _artworkRepo.CreateOneAsync(artwork); + return _mapper.Map(createdArtwork); + } + + public async Task> GetAllAsync(PaginationOptions paginationOptions) + { + if (paginationOptions.PageSize <= 0) + { + throw CustomException.BadRequest("PageSize should be greater than 0."); + } + + if (paginationOptions.PageNumber <= 0) + { + throw CustomException.BadRequest("PageNumber should be greater than 0."); + } + + var artworkList = await _artworkRepo.GetAllAsync(paginationOptions); + if (artworkList == null || !artworkList.Any()) + { + throw CustomException.NotFound("No artworks found."); + } + return _mapper.Map, List>(artworkList); + } + + public async Task GetByIdAsync(Guid id) + { + var artwork = await _artworkRepo.GetByIdAsync(id); + if (artwork == null) + { + throw CustomException.NotFound($"Artwork with id: {id} not found"); + } + return _mapper.Map(artwork); + } + + public async Task> GetByArtistIdAsync(Guid id) + { + var user = await _userRepo.GetByIdAsync(id) + ?? throw CustomException.NotFound($"User with id: {id} not found"); + if (user.Role.ToString() != UserRole.Artist.ToString()) + { + throw CustomException.BadRequest($"User with id: {id} is not an Artist"); + } + + var artworks = await _artworkRepo.GetByArtistIdAsync(id) + ?? throw CustomException.NotFound($"Artist with id: {id} has no artworks"); + + return _mapper.Map, List>(artworks); + } + + public async Task DeleteOneAsync(Guid id) + { + var foundArtwork = await _artworkRepo.GetByIdAsync(id); + if (foundArtwork == null) + { + throw CustomException.NotFound($"Artwork with id: {id} not found"); + } + bool isDeleted = await _artworkRepo.DeleteOneAsync(foundArtwork); + return isDeleted; + } + + public async Task UpdateOneAsync(Guid id, ArtworkUpdateDTO updateDto) + { + var foundArtwork = await _artworkRepo.GetByIdAsync(id); + if (foundArtwork == null) + { + throw CustomException.NotFound($"Artwork with id: {id} not found"); + } + + _mapper.Map(updateDto, foundArtwork); + var updatedArtwork = await _artworkRepo.UpdateOneAsync(foundArtwork); + return _mapper.Map(updatedArtwork); + } + + public async Task GetArtworkCountAsync() + { + return await _artworkRepo.CountAsync(); + } + + public async Task GetArtworkCountByArtistAsync(Guid artistId) + { + return await _artworkRepo.CountByArtistAsync(artistId); + } + } +} diff --git a/src/Services/artwork/IArtworkService.cs b/src/Services/artwork/IArtworkService.cs new file mode 100644 index 0000000000000000000000000000000000000000..523f54d29d676194494092eb21f2d325293653e2 --- /dev/null +++ b/src/Services/artwork/IArtworkService.cs @@ -0,0 +1,19 @@ +using Backend_Teamwork.src.Utils; +using static Backend_Teamwork.src.DTO.ArtworkDTO; + +namespace Backend_Teamwork.src.Services.artwork +{ + public interface IArtworkService + { + Task CreateOneAsync(Guid userId, ArtworkCreateDto artwork); + Task> GetAllAsync(PaginationOptions paginationOptions); + Task GetByIdAsync(Guid id); + Task> GetByArtistIdAsync(Guid id); + Task DeleteOneAsync(Guid id); + Task UpdateOneAsync(Guid id, ArtworkUpdateDTO updateArtwork); + + // New methods for counting + Task GetArtworkCountAsync(); // Get total count of artworks + Task GetArtworkCountByArtistAsync(Guid artistId); // Get count of artworks by a specific artist + } +} diff --git a/src/Services/booking/BookingService.cs b/src/Services/booking/BookingService.cs new file mode 100644 index 0000000000000000000000000000000000000000..8e61ce176701e0c65af4eec27358c3706674380e --- /dev/null +++ b/src/Services/booking/BookingService.cs @@ -0,0 +1,253 @@ +using AutoMapper; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Repository; +using Backend_Teamwork.src.Utils; +using static Backend_Teamwork.src.DTO.BookingDTO; +using static Backend_Teamwork.src.Entities.User; + +namespace Backend_Teamwork.src.Services.booking +{ + public class BookingService : IBookingService + { + private readonly BookingRepository _bookingRepository; + private readonly WorkshopRepository _workshopRepository; + private readonly PaymentRepository _paymentRepository; + + private readonly IMapper _mapper; + + public BookingService( + BookingRepository bookingRepository, + WorkshopRepository workshopRepository, + IMapper mapper + ) + { + _bookingRepository = bookingRepository; + _workshopRepository = workshopRepository; + _mapper = mapper; + } + + public async Task> GetAllAsync() + { + var bookings = await _bookingRepository.GetAllAsync(); + if (bookings.Count == 0) + { + throw CustomException.NotFound($"Bookings not found"); + } + return _mapper.Map, List>(bookings); + } + + public async Task GetByIdAsync(Guid id, Guid userId, string userRole) + { + var booking = await _bookingRepository.GetByIdAsync(id); + if (booking == null) + { + throw CustomException.NotFound($"Booking with id: {id} not found"); + } + if (userRole != UserRole.Admin.ToString() && booking.UserId != userId) + { + throw CustomException.Forbidden($"Not allowed to access booking with id: {id}"); + } + return _mapper.Map(booking); + } + + public async Task> GetByUserIdAsync(Guid userId) + { + var bookings = await _bookingRepository.GetByUserIdAsync(userId); + if (bookings.Count == 0) + { + throw CustomException.NotFound( + $"Bookings associated with userId: {userId} not found" + ); + } + return _mapper.Map, List>(bookings); + } + + public async Task> GetByStatusAsync(string status) + { + var bookings = await _bookingRepository.GetByStatusAsync(status); + if (bookings.Count == 0) + { + throw CustomException.NotFound($"Bookings with status: {status} not found"); + } + return _mapper.Map, List>(bookings); + } + + public async Task> GetByUserIdAndStatusAsync( + Guid userId, + string status + ) + { + var bookings = await _bookingRepository.GetByUserIdAndStatusAsync(userId, status); + if (bookings.Count == 0) + { + throw CustomException.NotFound($"Bookings with status: {status} not found"); + } + return _mapper.Map, List>(bookings); + } + + public async Task> GetWithPaginationAsync( + PaginationOptions paginationOptions + ) + { + var bookings = await _bookingRepository.GetWithPaginationAsync(paginationOptions); + if (bookings.Count == 0) + { + throw CustomException.NotFound($"Bookings not found"); + } + return _mapper.Map, List>(bookings); + } + + public async Task> GetByUserIdWithPaginationAsync( + PaginationOptions paginationOptions + ) + { + var bookings = await _bookingRepository.GetByUserIdWithPaginationAsync( + paginationOptions + ); + if (bookings.Count == 0) + { + throw CustomException.NotFound($"Bookings not found"); + } + return _mapper.Map, List>(bookings); + } + + public async Task CreateAsync(BookingCreateDto booking, Guid userId) + { + //1. check if the workshop is found + var workshop = await _workshopRepository.GetByIdAsync(booking.WorkshopId); + if (workshop == null) + { + throw CustomException.NotFound($"Workshp with id: {booking.WorkshopId} not found"); + } + //2. check if the workshop isn't available + if (!workshop.Availability) + { + throw CustomException.BadRequest($"Invalid booking"); + } + //3. check if the user already enrolled in this workshop + bool isFound = await _bookingRepository.GetByUserIdAndWorkshopIdAsync( + userId, + booking.WorkshopId + ); + if (isFound) + { + throw CustomException.BadRequest($"Invalid booking"); + } + //4. check if the user enrolled in other workshop at the same time + var workshops = await _workshopRepository.GetAllAsync(); + var foundWorkshop = workshops.FirstOrDefault(w => + (w.StartTime == workshop.StartTime && w.EndTime == workshop.EndTime) + || (w.StartTime < workshop.StartTime && w.EndTime > workshop.StartTime) + || (w.StartTime < workshop.EndTime && w.EndTime > workshop.EndTime) + ); + var isFound2 = false; + if (foundWorkshop != null) + { + isFound2 = await _bookingRepository.GetByUserIdAndWorkshopIdAsync( + userId, + foundWorkshop.Id + ); + } + if (isFound2) + { + throw CustomException.BadRequest($"Invalid booking"); + } + //create booking + var mappedBooking = _mapper.Map(booking); + mappedBooking.UserId = userId; + mappedBooking.Status = Status.Pending; + var createdBooking = await _bookingRepository.CreateAsync(mappedBooking); + return _mapper.Map(createdBooking); + } + + //after payment + public async Task ConfirmAsync(Guid id) + { + var booking = await _bookingRepository.GetByIdAsync(id); + if (booking == null) + { + throw CustomException.NotFound($"Booking with id: {id} not found"); + } + //1. check if the workshop isn't available + if (!booking.Workshop.Availability) + { + throw CustomException.BadRequest($"Invalid confirming"); + } + //2. check if the booking status isn't pending + if (booking.Status.ToString() != Status.Pending.ToString()) + { + throw CustomException.BadRequest($"Invalid confirming"); + } + //3. check if the user doesn't pay + //var payment = + + //confirm booking + booking.Status = Status.Confirmed; + var updatedBooking = await _bookingRepository.UpdateAsync(booking); + return _mapper.Map(updatedBooking); + } + + //after workshop becomes unavailable + public async Task> RejectAsync(Guid workshopId) + { + var workshop = await _workshopRepository.GetByIdAsync(workshopId); + //1. check if the workshop is found + if (workshop == null) + { + throw CustomException.NotFound($"Workshp with id: {workshopId} not found"); + } + //2. check if the workshop is available + if (workshop.Availability) + { + throw CustomException.BadRequest($"Invalid regecting"); + } + var bookings = await _bookingRepository.GetByWorkshopIdAndStatusAsync( + workshopId, + Status.Pending + ); + //3. check if there is booking with Pending Status + if (bookings == null) + { + throw CustomException.BadRequest($"Invalid regecting"); + } + foreach (var booking in bookings) + { + //reject booking + booking.Status = Status.Rejected; + var updatedBooking = await _bookingRepository.UpdateAsync(booking); + } + return _mapper.Map, List>(bookings); + } + + public async Task CancelAsync(Guid id, Guid userId) + { + var booking = await _bookingRepository.GetByIdAsync(id); + if (booking == null) + { + throw CustomException.NotFound($"Booking with id: {id} not found"); + } + //1. check if the booking belongs to the user + if (booking.UserId != userId) + { + throw CustomException.BadRequest($"Invalid canceling"); + } + //2. check if the workshop is available + if (!booking.Workshop.Availability) + { + throw CustomException.BadRequest($"Invalid canceling"); + } + //3. check if the booking status isn't pending + if (booking.Status.ToString() != Status.Pending.ToString()) + { + throw CustomException.BadRequest($"Invalid canceling"); + } + //4. check if the user pay + //var payment = + + //Cancel booking + booking.Status = Status.Canceled; + var updatedBooking = await _bookingRepository.UpdateAsync(booking); + return _mapper.Map(updatedBooking); + } + } +} diff --git a/src/Services/booking/IBookingService.cs b/src/Services/booking/IBookingService.cs new file mode 100644 index 0000000000000000000000000000000000000000..c2025c1061f5dc4f91f63822bebf42bc52bd12ee --- /dev/null +++ b/src/Services/booking/IBookingService.cs @@ -0,0 +1,22 @@ +using Backend_Teamwork.src.Utils; +using static Backend_Teamwork.src.DTO.BookingDTO; + +namespace Backend_Teamwork.src.Services.booking +{ + public interface IBookingService + { + Task> GetAllAsync(); + Task GetByIdAsync(Guid id, Guid userId, string userRole); + Task> GetByUserIdAsync(Guid userId); + Task> GetByStatusAsync(string status); + Task> GetByUserIdAndStatusAsync(Guid userId, string status); + Task> GetWithPaginationAsync(PaginationOptions paginationOptions); + Task> GetByUserIdWithPaginationAsync( + PaginationOptions paginationOptions + ); + Task CreateAsync(BookingCreateDto booking, Guid userId); + Task ConfirmAsync(Guid id); + Task> RejectAsync(Guid workshopId); + Task CancelAsync(Guid id, Guid userId); + } +} diff --git a/src/Services/category/CategoryService.cs b/src/Services/category/CategoryService.cs new file mode 100644 index 0000000000000000000000000000000000000000..6759100688ae6f8ed9623458aa52e594bc9558a1 --- /dev/null +++ b/src/Services/category/CategoryService.cs @@ -0,0 +1,124 @@ +using AutoMapper; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Repository; +using Backend_Teamwork.src.Utils; +using static Backend_Teamwork.src.DTO.CategoryDTO; + +namespace Backend_Teamwork.src.Services.category +{ + public class CategoryService : ICategoryService + { + private readonly CategoryRepository _categoryRepository; + private readonly ArtworkRepository _artworkRepository; + private readonly IMapper _mapper; + + public CategoryService( + CategoryRepository categoryRepository, + IMapper mapper, + ArtworkRepository artworkRepository + ) + { + _categoryRepository = categoryRepository; + _artworkRepository = artworkRepository; + _mapper = mapper; + } + + public async Task> GetAllAsync() + { + var categories = await _categoryRepository.GetAllAsync(); + if (categories.Count == 0) + { + throw CustomException.NotFound($"Categories not found"); + } + return _mapper.Map, List>(categories); + } + + public async Task GetByIdAsync(Guid id) + { + var foundCategory = await _categoryRepository.GetByIdAsync(id); + if (foundCategory == null) + { + throw CustomException.NotFound($"Category with id: {id} not found"); + } + return _mapper.Map(foundCategory); + } + + public async Task GetByNameAsync(string name) + { + var foundCategory = await _categoryRepository.GetByNameAsync(name); + if (foundCategory == null) + { + throw CustomException.NotFound($"Category with name: {name} not found"); + } + return _mapper.Map(foundCategory); + } + + public async Task> GetWithPaginationAsync( + PaginationOptions paginationOptions + ) + { + var foundCategories = await _categoryRepository.GetWithPaginationAsync( + paginationOptions + ); + if (foundCategories.Count == 0) + { + throw CustomException.NotFound($"Categories not found"); + } + return _mapper.Map, List>(foundCategories); + } + + public async Task> SortByNameAsync() + { + var categories = await _categoryRepository.SortByNameAsync(); + if (categories.Count == 0) + { + throw CustomException.NotFound($"Categories not found"); + } + return _mapper.Map, List>(categories); + } + + public async Task CreateAsync(CategoryCreateDto category) + { + var foundName = await _categoryRepository.GetByNameAsync(category.Name); + if (foundName != null) + { + throw CustomException.BadRequest($"Invalid category name"); + } + var mappedCategory = _mapper.Map(category); + var createdCategory = await _categoryRepository.CreateAsync(mappedCategory); + return _mapper.Map(createdCategory); + } + + public async Task UpdateAsync(Guid id, CategoryUpdateDto category) + { + var foundCategory = await _categoryRepository.GetByIdAsync(id); + var foundName = await _categoryRepository.GetByNameAsync(category.Name); + if (foundCategory == null) + { + throw CustomException.NotFound($"Category with id: {id} not found"); + } + if (foundName != null) + { + throw CustomException.BadRequest($"Invalid category name"); + } + _mapper.Map(category, foundCategory); + var updatedCategory = await _categoryRepository.UpdateAsync(foundCategory); + return _mapper.Map(updatedCategory); + } + + public async Task DeleteAsync(Guid id) + { + var foundCategory = await _categoryRepository.GetByIdAsync(id); + //var foundArtwork = await _artworkRepository.GetByCategoryIdAsync(foundCategory.Id); + if (foundCategory == null) + { + throw CustomException.NotFound($"Category with id: {id} not found"); + } + /*if (foundArtwork != null) + { + throw CustomException.NotFound($"Invalid deleting"); + }*/ + await _categoryRepository.DeleteAsync(foundCategory); + } + } +} diff --git a/src/Services/category/ICategoryService.cs b/src/Services/category/ICategoryService.cs new file mode 100644 index 0000000000000000000000000000000000000000..b8f99c21a3be7f96c5ebae7692ac57ab502cd210 --- /dev/null +++ b/src/Services/category/ICategoryService.cs @@ -0,0 +1,17 @@ +using Backend_Teamwork.src.Utils; +using static Backend_Teamwork.src.DTO.CategoryDTO; + +namespace Backend_Teamwork.src.Services.category +{ + public interface ICategoryService + { + Task> GetAllAsync(); + Task GetByIdAsync(Guid id); + Task GetByNameAsync(string name); + Task> GetWithPaginationAsync(PaginationOptions paginationOptions); + Task> SortByNameAsync(); + Task CreateAsync(CategoryCreateDto category); + Task UpdateAsync(Guid id, CategoryUpdateDto category); + Task DeleteAsync(Guid id); + } +} diff --git a/src/Services/order/IOrderService.cs b/src/Services/order/IOrderService.cs new file mode 100644 index 0000000000000000000000000000000000000000..8c191466b32db6273f55d4160abade1553009901 --- /dev/null +++ b/src/Services/order/IOrderService.cs @@ -0,0 +1,25 @@ +using Backend_Teamwork.src.Utils; +using static Backend_Teamwork.src.DTO.OrderDTO; + +namespace Backend_Teamwork.src.Services.order +{ + public interface IOrderService + { + // Get all + Task> GetAllAsync(); + Task> GetAllAsync(Guid id); + + // create + Task CreateOneAsync(Guid id, OrderCreateDto createDto); + + // Get by id + Task GetByIdAsync(Guid id); + Task GetByIdAsync(Guid id, Guid userId); + + // delete + Task DeleteOneAsync(Guid id); + Task UpdateOneAsync(Guid id, OrderUpdateDto updateDto); + Task> GetOrdersByPage(PaginationOptions paginationOptions); + Task> SortOrdersByDate(); + } +} diff --git a/src/Services/order/OrderService.cs b/src/Services/order/OrderService.cs new file mode 100644 index 0000000000000000000000000000000000000000..9648c128c86a17f87fda014dd30b7b97e059e609 --- /dev/null +++ b/src/Services/order/OrderService.cs @@ -0,0 +1,220 @@ +using AutoMapper; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Repository; +using Backend_Teamwork.src.Utils; +using static Backend_Teamwork.src.DTO.OrderDTO; + +namespace Backend_Teamwork.src.Services.order +{ + public class OrderService : IOrderService + { + private readonly OrderRepository _orderRepository; + private readonly IMapper _mapper; + + private readonly ArtworkRepository _artworkRepository; + + public OrderService( + OrderRepository OrderRepository, + IMapper mapper, + ArtworkRepository artworkRepository + ) + { + _orderRepository = OrderRepository; + _mapper = mapper; + _artworkRepository = artworkRepository; + } + + //----------------------------------------------------- + + // Retrieves all orders (Only Admin) + public async Task> GetAllAsync() + { + var OrderList = await _orderRepository.GetAllAsync(); + if (OrderList.Count == 0) + { + throw CustomException.NotFound($"Orders not found"); + } + return _mapper.Map, List>(OrderList); + } + + // Retrieves all orders + public async Task> GetAllAsync(Guid id) + { + if (id == Guid.Empty) + { + throw CustomException.BadRequest("Invalid order ID"); + } + var orders = await _orderRepository.GetOrdersByUserIdAsync(id); + if (orders == null || !orders.Any()) + { + throw CustomException.NotFound($"No orders found for user with id: {id}"); + } + return _mapper.Map, List>(orders); + } + + //----------------------------------------------------- + + // Creates a new order + public async Task CreateOneAsync(Guid userId, OrderCreateDto createDto) + { + // Validate the createDto object + if ( + createDto == null + || createDto.OrderDetails == null + || !createDto.OrderDetails.Any() + ) + { + throw CustomException.BadRequest( + "Invalid order data or no artworks provided in the order." + ); + } + + decimal totalAmount = 0; // Initialize total amount + + // Process each artwork in the order + foreach (var orderDetail in createDto.OrderDetails) + { + // Fetch the artwork by its ID + var artwork = await _artworkRepository.GetByIdAsync(orderDetail.ArtworkId); // Using ArtworkId + + // Validate if the artwork exists + if (artwork == null) + { + throw CustomException.NotFound( + $"Artwork with ID: {orderDetail.ArtworkId} not found." + ); + } + + // Check if there is enough stock for the requested quantity + if (artwork.Quantity < orderDetail.Quantity) + { + throw CustomException.BadRequest( + $"Artwork {artwork.Title} does not have enough stock. Requested: {orderDetail.Quantity}, Available: {artwork.Quantity}." + ); + } + + // Reduce artwork stock + artwork.Quantity -= orderDetail.Quantity; + + // Update the artwork quantity in the repository + await _artworkRepository.UpdateOneAsync(artwork); + + // Calculate the total amount for this order detail + decimal detailAmount = artwork.Price * orderDetail.Quantity; + + // Add this amount to the total amount + totalAmount += detailAmount; + } + + // Set the order creation time + createDto.CreatedAt = DateTime.UtcNow; + + var newOrder = _mapper.Map(createDto); + + // Set the user ID on the new order + newOrder.UserId = userId; + newOrder.TotalAmount = totalAmount; + + // Save the order to the repository + var createdOrder = await _orderRepository.CreateOneAsync(newOrder); + + // Return the created order as a DTO + return _mapper.Map(createdOrder); + } + + //----------------------------------------------------- + + // Retrieves a order by their ID (Only Admin) + public async Task GetByIdAsync(Guid id) + { + if (id == Guid.Empty) + { + throw CustomException.BadRequest("Invalid order ID"); + } + var foundOrder = await _orderRepository.GetByIdAsync(id); + if (foundOrder == null) + { + throw CustomException.NotFound($"Order with ID {id} not found."); + } + return _mapper.Map(foundOrder); + } + + // Retrieves a order by their ID + public async Task GetByIdAsync(Guid id, Guid userId) + { + if (id == Guid.Empty) + { + throw CustomException.BadRequest("Invalid order ID"); + } + var foundOrder = await _orderRepository.GetByIdAsync(id); + if (foundOrder == null) + { + throw CustomException.NotFound($"Order with ID {id} not found."); + } + if (foundOrder.UserId != userId) + { + throw CustomException.Forbidden("You are not authorized to view this order."); + } + + return _mapper.Map(foundOrder); + } + + //----------------------------------------------------- + + // Deletes a order by their ID + public async Task DeleteOneAsync(Guid id) + { + var foundOrder = await _orderRepository.GetByIdAsync(id); + if (foundOrder == null) + { + throw CustomException.NotFound($"Order with ID {id} not found."); + } + return await _orderRepository.DeleteOneAsync(foundOrder); + } + + // Updates a order by their ID + public async Task UpdateOneAsync(Guid id, OrderUpdateDto updateDto) + { + var foundOrder = await _orderRepository.GetByIdAsync(id); + if (foundOrder == null) + { + throw CustomException.NotFound($"Order with ID {id} not found."); + } + + // Map the update DTO to the existing Order entity + _mapper.Map(updateDto, foundOrder); + return await _orderRepository.UpdateOneAsync(foundOrder); + } + + public async Task> GetOrdersByPage(PaginationOptions paginationOptions) + { + // Validate pagination options + if (paginationOptions.PageSize <= 0) + { + throw CustomException.BadRequest("Page Size should be greater than 0."); + } + + if (paginationOptions.PageNumber < 0) + { + throw CustomException.BadRequest("Page Number should be 0 or greater."); + } + var OrderList = await _orderRepository.GetAllAsync(paginationOptions); + if (OrderList == null || !OrderList.Any()) + { + throw CustomException.NotFound("Orders not found"); + } + return _mapper.Map, List>(OrderList); + } + + public async Task> SortOrdersByDate() + { + var orders = await _orderRepository.GetAllAsync(); + if (orders.Count == 0) + { + throw CustomException.NotFound("Orders not found"); + } + var sortedOrders=orders.OrderBy(x => x.CreatedAt).ToList(); + return _mapper.Map, List>(sortedOrders); + } + } +} diff --git a/src/Services/payment/IPaymentService.cs b/src/Services/payment/IPaymentService.cs new file mode 100644 index 0000000000000000000000000000000000000000..5e107142bd9651878613eba77421d02b2190f678 --- /dev/null +++ b/src/Services/payment/IPaymentService.cs @@ -0,0 +1,14 @@ +using static Backend_Teamwork.src.DTO.PaymentDTO; + +namespace Backend_Teamwork.src.Services.payment +{ + public interface IPaymentService + { + + Task CreateOneAsync(PaymentCreateDTO createpaymentDto); + Task> GetAllAsync(); + Task GetByIdAsync(Guid id); + Task DeleteOneAsync(Guid id); + Task UpdateOneAsync(Guid id, PaymentUpdateDTO updatepaymentDto); + } +} \ No newline at end of file diff --git a/src/Services/payment/PaymentService.cs b/src/Services/payment/PaymentService.cs new file mode 100644 index 0000000000000000000000000000000000000000..8c452618b00caa5137dca66c60044c1908c1ebcc --- /dev/null +++ b/src/Services/payment/PaymentService.cs @@ -0,0 +1,79 @@ +using AutoMapper; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Repository; +using Backend_Teamwork.src.Utils; +using static Backend_Teamwork.src.DTO.PaymentDTO; + +namespace Backend_Teamwork.src.Services.payment +{ + public class PaymentService : IPaymentService + { + private readonly PaymentRepository _paymentRepository; + private readonly OrderRepository _orderRepository; + private readonly BookingRepository _bookingRepository; + + private readonly IMapper _mapper; + + public PaymentService( + PaymentRepository paymentRepository, + IMapper mapper, + OrderRepository orderRepository, + BookingRepository bookingRepository + ) + { + _paymentRepository = paymentRepository; + _orderRepository= orderRepository; + _bookingRepository= bookingRepository; + _mapper = mapper; + } + + public async Task CreateOneAsync(PaymentCreateDTO createpaymentDto) + { + var payment = _mapper.Map(createpaymentDto); + var paymentCreated = await _paymentRepository.CreateOneAsync(payment); + return _mapper.Map(paymentCreated); + } + + public async Task> GetAllAsync() + { + var paymentList = await _paymentRepository.GetAllAsync(); + if (paymentList == null || !paymentList.Any()) + { + throw CustomException.NotFound($"Payments not found"); + } + return _mapper.Map, List>(paymentList); + } + + public async Task GetByIdAsync(Guid id) + { + var foundpayment = await _paymentRepository.GetByIdAsync(id); + if (foundpayment == null) + { + throw CustomException.NotFound($"Payment with id: {id} not found"); + } + return _mapper.Map(foundpayment); + } + + public async Task DeleteOneAsync(Guid id) + { + var foundpayment = await _paymentRepository.GetByIdAsync(id); + if (foundpayment == null) + { + throw CustomException.NotFound($"Payment with id: {id} not found"); + } + return await _paymentRepository.DeleteOneAsync(foundpayment); + ; + } + + public async Task UpdateOneAsync(Guid id, PaymentUpdateDTO paymentupdateDto) + { + var foundpayment = await _paymentRepository.GetByIdAsync(id); + if (foundpayment == null) + { + throw CustomException.NotFound($"Payment with id: {id} not found"); + } + _mapper.Map(paymentupdateDto, foundpayment); + return await _paymentRepository.UpdateOneAsync(foundpayment); + } + } +} diff --git a/src/Services/user/IUserService.cs b/src/Services/user/IUserService.cs new file mode 100644 index 0000000000000000000000000000000000000000..9872b3747ce1661f2aef430bb7d41a228354a91c --- /dev/null +++ b/src/Services/user/IUserService.cs @@ -0,0 +1,32 @@ +using Backend_Teamwork.src.Utils; +using static Backend_Teamwork.src.DTO.UserDTO; + +namespace Backend_Teamwork.src.Services.user +{ + public interface IUserService + { + // Get all + Task> GetAllAsync(PaginationOptions paginationOptions); + + // Task> GetUsersByPage(PaginationOptions paginationOptions); + + // create + Task CreateOneAsync(UserCreateDto createDto); + + // Get by id + //Task GetOneByIdAsync(Guid userId); + Task GetByIdAsync(Guid id); + + // delete + Task DeleteOneAsync(Guid id); + + //Task UpdateOneByIdAsync(Guid userId, UserUpdateDto updateDto); + Task UpdateOneAsync(Guid id, UserUpdateDto updateDto); + + Task GetByEmailAsync(string email); + Task GetByPhoneNumberAsync(string phoneNumber); + Task GetTotalUsersCountAsync(); + + Task SignInAsync(UserSigninDto createDto); + } +} diff --git a/src/Services/user/UserService.cs b/src/Services/user/UserService.cs new file mode 100644 index 0000000000000000000000000000000000000000..18daf2a125ddf3ffe0ae8ab3674976f3aa3d8405 --- /dev/null +++ b/src/Services/user/UserService.cs @@ -0,0 +1,245 @@ +using AutoMapper; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Repository; +using Backend_Teamwork.src.Utils; +using static Backend_Teamwork.src.DTO.UserDTO; +using static Backend_Teamwork.src.Entities.User; + +namespace Backend_Teamwork.src.Services.user +{ + public class UserService : IUserService + { + private readonly UserRepository _userRepository; + private readonly IMapper _mapper; + private readonly IConfiguration _configuration; + + public UserService( + UserRepository UserRepository, + IMapper mapper, + IConfiguration configuration + ) + { + _configuration = configuration; + _userRepository = UserRepository; + _mapper = mapper; + } + + // Retrieves all users + public async Task> GetAllAsync(PaginationOptions paginationOptions) + { + // Validate pagination options + if (paginationOptions.PageSize <= 0) + { + throw CustomException.BadRequest("PageSize should be greater than 0."); + } + + if (paginationOptions.PageNumber <= 0) + { + throw CustomException.BadRequest("PageNumber should be greater than 0."); + } + + var UserList = await _userRepository.GetAllAsync(paginationOptions); + if (!UserList.Any() || UserList == null) + { + throw CustomException.NotFound($"Users not found"); + } + return _mapper.Map, List>(UserList); + } + + // Gets the total count of users + public async Task GetTotalUsersCountAsync() + { + return await _userRepository.GetCountAsync(); + } + + // Creates a new user + public async Task CreateOneAsync(UserCreateDto createDto) + { + if (createDto == null) + { + throw CustomException.BadRequest("User data cannot be null."); + } + if ( + createDto + .Role.ToString() + .Equals(UserRole.Admin.ToString(), StringComparison.OrdinalIgnoreCase) + ) + { + throw CustomException.UnAuthorized( + "Only admin users can create other admin accounts." + ); + } + // Hash password before saving to the database + PasswordUtils.HashPassword( + createDto.Password, + out string hashedPassword, + out byte[] salt + ); + var user = _mapper.Map(createDto); + user.Password = hashedPassword; + user.Salt = salt; + + var UserCreated = await _userRepository.CreateOneAsync(user); + if (UserCreated == null) + { + throw CustomException.BadRequest("Failed to create user."); + } + return _mapper.Map(UserCreated); + } + + //----------------------------------------------------- + + // Retrieves a user by their ID (Admin,customer,artist) + public async Task GetByIdAsync(Guid userId) + { + var foundUser = await _userRepository.GetByIdAsync(userId); + if (foundUser == null) + { + throw CustomException.NotFound($"User with id: {userId} not found"); + } + return _mapper.Map(foundUser); + } + + //----------------------------------------------------- + + // Deletes a user by their ID + public async Task DeleteOneAsync(Guid id) + { + if (id == Guid.Empty) + { + throw CustomException.BadRequest("Invalid user ID"); + } + var foundUser = await _userRepository.GetByIdAsync(id); + if (foundUser == null) + { + throw CustomException.NotFound($"User with ID {id} not found."); + } + var DeletedUser = await _userRepository.DeleteOneAsync(foundUser); + + // Check if the delete was successful + if (!DeletedUser) + { + throw CustomException.BadRequest("Failed to delete user."); + } + + return DeletedUser; + } + + //----------------------------------------------------- + + // Updates a user by their ID + public async Task UpdateOneAsync(Guid id, UserUpdateDto updateDto) + { + if (id == Guid.Empty) + { + throw CustomException.BadRequest("Invalid user ID"); + } + if (updateDto == null) + { + throw CustomException.BadRequest("Update data cannot be null"); + } + + var foundUser = await _userRepository.GetByIdAsync(id); + if (foundUser == null) + { + throw CustomException.NotFound($"User with ID {id} not found."); + } + + // Map the update DTO to the existing User entity + _mapper.Map(updateDto, foundUser); + + // Hash password before saving to the database if it's provided + if (!string.IsNullOrEmpty(updateDto.Password)) + { + PasswordUtils.HashPassword( + updateDto.Password, + out string hashedPassword, + out byte[] salt + ); + foundUser.Password = hashedPassword; + foundUser.Salt = salt; + } + + var updatedUser = await _userRepository.UpdateOneAsync(foundUser); + + // Check if the update was successful + if (!updatedUser) + { + throw CustomException.BadRequest("Failed to update user."); + } + + return updatedUser; + } + + //----------------------------------------------------- + + // Retrieves a user by their email + public async Task GetByEmailAsync(string email) + { + if (string.IsNullOrWhiteSpace(email)) + { + throw CustomException.BadRequest("Email is required"); + } + var user = await _userRepository.GetByEmailAsync(email); + if (user == null) + { + throw CustomException.NotFound($"User with email {email} not found."); + } + return _mapper.Map(user); + } + + // Retrieves a user by their phone number + public async Task GetByPhoneNumberAsync(string phoneNumber) + { + if (string.IsNullOrWhiteSpace(phoneNumber)) + { + throw CustomException.BadRequest("Phone Number is required"); + } + + var user = await _userRepository.GetByPhoneNumberAsync(phoneNumber); + if (user == null) + { + throw CustomException.NotFound("User not found."); + } + return _mapper.Map(user); + ; + } + + // Signs in a user with their credentials + public async Task SignInAsync(UserSigninDto signinDto) + { + if (signinDto == null) + { + throw CustomException.BadRequest("User data cannot be null."); + } + + var foundUser = await _userRepository.GetByEmailAsync(signinDto.Email); + if (foundUser == null) + { + throw CustomException.NotFound($"User with E-mail: {signinDto.Email} not found."); + } + + // Verify the password + bool isMatched = PasswordUtils.VerifyPassword( + signinDto.Password, + foundUser.Password, + foundUser.Salt + ); + + if (!isMatched) + { + throw CustomException.UnAuthorized($"Unauthorized access."); + } + + var TokenUtil = new TokenUtils(_configuration); + var token = TokenUtil.GenerateToken(foundUser); + + if (string.IsNullOrEmpty(token)) + { + throw CustomException.UnAuthorized("Failed to generate token."); + } + + return token; + } + } +} diff --git a/src/Services/workshop/IWorkshopService.cs b/src/Services/workshop/IWorkshopService.cs new file mode 100644 index 0000000000000000000000000000000000000000..e675b959027ac96542f369cf03ed638aee1a9262 --- /dev/null +++ b/src/Services/workshop/IWorkshopService.cs @@ -0,0 +1,15 @@ +using Backend_Teamwork.src.Utils; +using static Backend_Teamwork.src.DTO.WorkshopDTO; + +namespace Backend_Teamwork.src.Services.workshop +{ + public interface IWorkshopService + { + Task CreateOneAsync(Guid artistId, WorkshopCreateDTO createworkshopDto); + Task> GetAllAsync(); + Task> GetAllAsync(PaginationOptions paginationOptions); + Task GetByIdAsync(Guid id); + Task DeleteOneAsync(Guid id); + Task UpdateOneAsync(Guid id, WorkshopUpdateDTO updateworkshopDto); + } +} diff --git a/src/Services/workshop/WorkshopService.cs b/src/Services/workshop/WorkshopService.cs new file mode 100644 index 0000000000000000000000000000000000000000..34973f99cc53466408a46219194b4961438a3142 --- /dev/null +++ b/src/Services/workshop/WorkshopService.cs @@ -0,0 +1,93 @@ +using AutoMapper; +using Backend_Teamwork.src.Entities; +using Backend_Teamwork.src.Repository; +using Backend_Teamwork.src.Utils; +using static Backend_Teamwork.src.DTO.WorkshopDTO; + +namespace Backend_Teamwork.src.Services.workshop +{ + public class WorkshopService : IWorkshopService + { + protected readonly WorkshopRepository _workshopRepo; + protected readonly IMapper _mapper; + + public WorkshopService(WorkshopRepository workshopRepo, IMapper mapper) + { + _workshopRepo = workshopRepo; + _mapper = mapper; + } + + public async Task CreateOneAsync( + Guid artistId, + WorkshopCreateDTO createworkshopDto + ) + { + var workshop = _mapper.Map(createworkshopDto); + workshop.UserId = artistId; + var workshopCreated = await _workshopRepo.CreateOneAsync(workshop); + return _mapper.Map(workshopCreated); + } + + public async Task> GetAllAsync() + { + var workshopList = await _workshopRepo.GetAllAsync(); + if (workshopList == null || !workshopList.Any()) + { + throw CustomException.NotFound("Workshops not found"); + } + return _mapper.Map, List>(workshopList); + } + + public async Task> GetAllAsync(PaginationOptions paginationOptions) + { + // Validate pagination options + if (paginationOptions.PageSize <= 0) + { + throw CustomException.BadRequest("Page Size should be greater than 0."); + } + + if (paginationOptions.PageNumber < 0) + { + throw CustomException.BadRequest("Page Number should be 0 or greater."); + } + var workshopList = await _workshopRepo.GetAllAsync(paginationOptions); + if (workshopList == null || !workshopList.Any()) + { + throw CustomException.NotFound("Workshops not found"); + } + return _mapper.Map, List>(workshopList); + } + + public async Task GetByIdAsync(Guid id) + { + var foundworkshop = await _workshopRepo.GetByIdAsync(id); + if (foundworkshop == null) + { + throw CustomException.NotFound($"Workshop with ID {id} not found."); + } + return _mapper.Map(foundworkshop); + } + + public async Task DeleteOneAsync(Guid id) + { + var foundworkshop = await _workshopRepo.GetByIdAsync(id); + if (foundworkshop == null) + { + throw CustomException.NotFound($"Workshop with ID {id} not found."); + } + return await _workshopRepo.DeleteOneAsync(foundworkshop); + ; + } + + public async Task UpdateOneAsync(Guid id, WorkshopUpdateDTO workshopupdateDto) + { + var foundworkshop = await _workshopRepo.GetByIdAsync(id); + if (foundworkshop == null) + { + throw CustomException.NotFound($"Workshop with ID {id} not found."); + } + _mapper.Map(workshopupdateDto, foundworkshop); + return await _workshopRepo.UpdateOneAsync(foundworkshop); + } + } +} diff --git a/src/Utils/AtLeastOneRequiredAttribute.cs b/src/Utils/AtLeastOneRequiredAttribute.cs new file mode 100644 index 0000000000000000000000000000000000000000..083ce330e7970e3dcaae7f9b627636c06189ba77 --- /dev/null +++ b/src/Utils/AtLeastOneRequiredAttribute.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; + +namespace Backend_Teamwork.src.Utils +{ + public class AtLeastOneRequiredAttribute : ValidationAttribute + { + protected override ValidationResult? IsValid( + object? value, + ValidationContext validationContext + ) + { + var properties = validationContext.ObjectType.GetProperties(); + foreach (var property in properties) + { + var propertyValue = property.GetValue(validationContext.ObjectInstance); + if (propertyValue != null && !string.IsNullOrWhiteSpace(propertyValue.ToString())) + { + return ValidationResult.Success; // At least one property is set + } + } + return new ValidationResult("At least one property must be updated."); + } + } +} diff --git a/src/Utils/CustomException.cs b/src/Utils/CustomException.cs new file mode 100644 index 0000000000000000000000000000000000000000..a44812d95674939d8bcedd1cc95eaa9360ba6a57 --- /dev/null +++ b/src/Utils/CustomException.cs @@ -0,0 +1,39 @@ + +namespace Backend_Teamwork.src.Utils +{ + public class CustomException : Exception + { + public int StatusCode { get; set; } + + public CustomException(int statusCode, string message) + : base(message) + { + StatusCode = statusCode; + } + + public static CustomException NotFound(string message) + { + return new CustomException(404, message); + } + + public static CustomException BadRequest(string message) + { + return new CustomException(400, message); + } + + public static CustomException UnAuthorized(string message) + { + return new CustomException(401, message); + } + + public static CustomException Forbidden(string message) + { + return new CustomException(403, message); + } + + public static CustomException InternalServer(string message) + { + return new CustomException(500, message); + } + } +} diff --git a/src/Utils/MapperProfile.cs b/src/Utils/MapperProfile.cs new file mode 100644 index 0000000000000000000000000000000000000000..52f5194048f6245d749f2a78dd5b7e296ad72631 --- /dev/null +++ b/src/Utils/MapperProfile.cs @@ -0,0 +1,75 @@ +using AutoMapper; +using Backend_Teamwork.src.Entities; +using static Backend_Teamwork.src.DTO.ArtworkDTO; +using static Backend_Teamwork.src.DTO.BookingDTO; +using static Backend_Teamwork.src.DTO.CategoryDTO; +using static Backend_Teamwork.src.DTO.OrderDetailDTO; +using static Backend_Teamwork.src.DTO.OrderDTO; +using static Backend_Teamwork.src.DTO.PaymentDTO; +using static Backend_Teamwork.src.DTO.UserDTO; +using static Backend_Teamwork.src.DTO.WorkshopDTO; + +namespace Backend_Teamwork.src.Utils +{ + public class MapperProfile : Profile + { + public MapperProfile() + { + CreateMap(); + CreateMap(); + CreateMap() + .ForMember( + dest => dest.Description, + opt => opt.Condition(src => src.Description != null) + ) + .ForAllMembers(opts => + opts.Condition((src, dest, srcProperty) => srcProperty != null) + ); + + CreateMap(); + CreateMap(); + CreateMap() + .ForAllMembers(opts => + opts.Condition((src, dest, srcProperty) => srcProperty != null) + ); + + CreateMap(); + CreateMap(); + CreateMap() + .ForAllMembers(opts => + opts.Condition((src, dest, srcProperty) => srcProperty != null) + ); + + CreateMap() + .ForMember(dest => dest.User, opt => opt.MapFrom(src => src.User)); + CreateMap(); + CreateMap() + .ForAllMembers(opts => + opts.Condition((src, dest, srcProperty) => srcProperty != null) + ); + + CreateMap(); + CreateMap(); + CreateMap() + .ForAllMembers(opts => + opts.Condition((src, dest, srcProperty) => srcProperty != null) + ); + + CreateMap(); + CreateMap(); + CreateMap() + .ForAllMembers(opts => + opts.Condition((src, dest, srcProperty) => srcProperty != null) + ); + + CreateMap(); + CreateMap() + .ForAllMembers(opts => + opts.Condition((src, dest, srcProperty) => srcProperty != null) + ); + + CreateMap(); + CreateMap(); + } + } +} diff --git a/src/Utils/PaginationOptions.cs b/src/Utils/PaginationOptions.cs new file mode 100644 index 0000000000000000000000000000000000000000..99cfd646dbf0465effa6b42752adbbebc1c58182 --- /dev/null +++ b/src/Utils/PaginationOptions.cs @@ -0,0 +1,20 @@ + +namespace Backend_Teamwork.src.Utils +{ + public class PaginationOptions + { + // Pagination + public int PageSize { get; set; } = 10; + public int PageNumber { get; set; } = 1; + + // Search + public string Search { get; set; } = string.Empty; + + // Sort + public string SortOrder { get; set; } = string.Empty;// "", "name_desc", "date_desc", "date", "price_desc", "price" + + // Price range + public decimal? LowPrice { get; set; } = 0; + public decimal? HighPrice { get; set; } = 10000; + } +} diff --git a/src/Utils/PasswordUtils.cs b/src/Utils/PasswordUtils.cs new file mode 100644 index 0000000000000000000000000000000000000000..fcd86a368dedae749434855233871cf6aaed2915 --- /dev/null +++ b/src/Utils/PasswordUtils.cs @@ -0,0 +1,21 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Backend_Teamwork.src.Utils +{ + public class PasswordUtils + { + public static string HashPassword(string originalPassword, out string hashedPassword, out byte[] salt) + { + var hmac = new HMACSHA256(); + salt = hmac.Key; + hashedPassword = BitConverter.ToString(hmac.ComputeHash(Encoding.UTF8.GetBytes(originalPassword))); + return hashedPassword; + } + public static bool VerifyPassword(string originalPassword, string hashedPassword, byte[] salt) + { + var hmac = new HMACSHA256(salt); + return BitConverter.ToString(hmac.ComputeHash(Encoding.UTF8.GetBytes(originalPassword))) == hashedPassword; + } + } +} \ No newline at end of file diff --git a/src/Utils/TokenUtils.cs b/src/Utils/TokenUtils.cs new file mode 100644 index 0000000000000000000000000000000000000000..8241ce2155ae4220dbf3b323127bbb51fa511c30 --- /dev/null +++ b/src/Utils/TokenUtils.cs @@ -0,0 +1,58 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Backend_Teamwork.src.Entities; +using Microsoft.IdentityModel.Tokens; + +namespace Backend_Teamwork.src.Utils +{ + public class TokenUtils + { + private readonly IConfiguration _configuration; + + public TokenUtils(IConfiguration configuration) + { + _configuration = configuration; + } + + public string GenerateToken(User user) + { + // key + var key = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(_configuration.GetSection("Jwt:Key").Value!) + ); + var SigningCredentials = new SigningCredentials( + key, + SecurityAlgorithms.HmacSha256Signature + ); + + // subject = list of claims, never put password in claims + var claims = new List + { + new Claim(ClaimTypes.Email, user.Email), + new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), + new Claim(ClaimTypes.Role, user.Role.ToString()), + }; + // use _config to access value in appsetting + var issure = _configuration.GetSection("Jwt:Issuer").Value; + var audience = _configuration.GetSection("Jwt:Audience").Value; + var descriptor = new SecurityTokenDescriptor + { + // header + Issuer = issure, + Audience = audience, + Expires = DateTime.Now.AddMinutes(60), // handle date here + + // payload + Subject = new ClaimsIdentity(claims), + + // verify + SigningCredentials = SigningCredentials, + }; + var tokenHandler = new JwtSecurityTokenHandler(); + var token = tokenHandler.CreateToken(descriptor); + + return tokenHandler.WriteToken(token); + } + } +} diff --git a/wait-for-postgres.sh b/wait-for-postgres.sh new file mode 100644 index 0000000000000000000000000000000000000000..15c2e15c5ed0e74e97fc5fd1b8f2e68ea3edc8b1 --- /dev/null +++ b/wait-for-postgres.sh @@ -0,0 +1,5 @@ +#!/bin/bash +until pg_isready -h localhost -p 5432; do + echo "Waiting for PostgreSQL to be ready..." + sleep 2 +done