├── src ├── Presentation │ ├── SpaWeb │ │ ├── public │ │ │ ├── robots.txt │ │ │ ├── favicon.ico │ │ │ ├── logo192.png │ │ │ ├── logo512.png │ │ │ ├── manifest.json │ │ │ └── index.html │ │ ├── src │ │ │ ├── components │ │ │ │ ├── DateTimePicker │ │ │ │ │ └── DateTimePicker.css │ │ │ │ ├── Home │ │ │ │ │ ├── Header │ │ │ │ │ │ └── Header.css │ │ │ │ │ ├── Home.jsx │ │ │ │ │ ├── HottestItems │ │ │ │ │ │ ├── HottestItems.css │ │ │ │ │ │ └── HottestItems.jsx │ │ │ │ │ └── LiveItems │ │ │ │ │ │ └── Pictures │ │ │ │ │ │ ├── PictureContainer.css │ │ │ │ │ │ └── PictureContainer.jsx │ │ │ │ ├── Items │ │ │ │ │ ├── List │ │ │ │ │ │ ├── Search │ │ │ │ │ │ │ └── Search.css │ │ │ │ │ │ ├── Container │ │ │ │ │ │ │ └── Container.css │ │ │ │ │ │ ├── Header │ │ │ │ │ │ │ ├── Header.jsx │ │ │ │ │ │ │ └── Header.css │ │ │ │ │ │ └── List.jsx │ │ │ │ │ ├── Create │ │ │ │ │ │ └── Create.css │ │ │ │ │ ├── DeleteModal.jsx │ │ │ │ │ └── Details │ │ │ │ │ │ ├── UserActionsContainer.jsx │ │ │ │ │ │ └── Details.jsx │ │ │ │ ├── Error │ │ │ │ │ ├── Error.css │ │ │ │ │ ├── NotFound.jsx │ │ │ │ │ └── NetworkError.jsx │ │ │ │ ├── Bid │ │ │ │ │ ├── BidHigherButton.jsx │ │ │ │ │ └── Chat │ │ │ │ │ │ ├── Chat.css │ │ │ │ │ │ └── Chat.jsx │ │ │ │ ├── PrivateRoute.jsx │ │ │ │ └── Account │ │ │ │ │ └── ConfirmAccount.jsx │ │ │ ├── services │ │ │ │ ├── categoriesService.js │ │ │ │ ├── bidService.js │ │ │ │ ├── picturesService.js │ │ │ │ └── adminService.js │ │ │ ├── utils │ │ │ │ ├── helpers │ │ │ │ │ ├── slug.js │ │ │ │ │ └── localStorage.js │ │ │ │ └── hooks │ │ │ │ │ ├── useTime.js │ │ │ │ │ ├── useDebounce.js │ │ │ │ │ ├── useItemsSearch.js │ │ │ │ │ ├── useCounter.js │ │ │ │ │ └── authHook.js │ │ │ ├── index.css │ │ │ ├── index.js │ │ │ └── App.js │ │ ├── .gitignore │ │ ├── .env │ │ └── package.json │ └── Api │ │ ├── ApiConstants.cs │ │ ├── appsettings.Development.json │ │ ├── Models │ │ ├── Errors │ │ │ ├── ErrorModel.cs │ │ │ └── ValidationErrorModel.cs │ │ ├── UsersFilter.cs │ │ ├── PaginationQuery.cs │ │ └── ItemsFilter.cs │ │ ├── SwaggerExamples │ │ ├── BadRequestErrorModel.cs │ │ ├── NotFoundErrorModel.cs │ │ ├── Requests │ │ │ ├── Users │ │ │ │ ├── LoginUserRequestExample.cs │ │ │ │ ├── CreateUserRequestExample.cs │ │ │ │ └── RefreshTokenRequestExample.cs │ │ │ ├── Admin │ │ │ │ ├── CreateAdminRequestExample.cs │ │ │ │ └── DeleteAdminRequestExample.cs │ │ │ ├── Bids │ │ │ │ └── CreateBidRequestExample.cs │ │ │ ├── Pictures │ │ │ │ ├── DeletePictureRequestExample.cs │ │ │ │ └── CreatePictureRequestExample.cs │ │ │ └── Items │ │ │ │ ├── UpdateItemRequestExample.cs │ │ │ │ └── CreateItemRequestExample.cs │ │ └── Responses │ │ │ ├── Items │ │ │ ├── SuccessfulItemCreateResponse.cs │ │ │ └── ItemDetails.cs │ │ │ ├── NotFoundResponseModel.cs │ │ │ ├── BadRequestResponseModel.cs │ │ │ ├── Pictures │ │ │ ├── GetPictureDetailsResponse.cs │ │ │ └── SuccessfulPictureUploadResponseModel.cs │ │ │ ├── Users │ │ │ └── LoginUserRequestResponseModel.cs │ │ │ ├── Admin │ │ │ └── SuccessfulAdminGetRequestResponseModel.cs │ │ │ └── Categories │ │ │ └── ListCategoriesSuccessfulResponse.cs │ │ ├── Controllers │ │ ├── BaseController.cs │ │ ├── CategoriesController.cs │ │ └── BidsController.cs │ │ ├── Program.cs │ │ ├── Extensions │ │ ├── ApplicationBuilderExtensions.cs │ │ └── ConfigurationExtensions.cs │ │ ├── appsettings.json │ │ ├── Services │ │ ├── CurrentUserService.cs │ │ ├── ResponseCacheService.cs │ │ └── Hosted │ │ │ └── MigrateDatabaseHostedService.cs │ │ ├── MiddleWares │ │ └── AuthorizationHeaderMiddleware.cs │ │ ├── Api.csproj │ │ └── Hubs │ │ └── BidHub.cs ├── Core │ ├── Common │ │ ├── README.md │ │ ├── AutoMapping │ │ │ └── Interfaces │ │ │ │ ├── IMapWith.cs │ │ │ │ └── IHaveCustomMapping.cs │ │ ├── IDateTime.cs │ │ ├── Common.csproj │ │ └── ModelConstants.cs │ ├── Domain │ │ ├── README.md │ │ ├── Domain.csproj │ │ ├── Entities │ │ │ ├── Picture.cs │ │ │ ├── Category.cs │ │ │ ├── AuctionUser.cs │ │ │ ├── Bid.cs │ │ │ ├── SubCategory.cs │ │ │ ├── RefreshToken.cs │ │ │ └── Item.cs │ │ └── Common │ │ │ └── AuditableEntity.cs │ └── Application │ │ ├── Common │ │ ├── Exceptions │ │ │ ├── UnauthorizedException.cs │ │ │ ├── BadRequestException.cs │ │ │ ├── DeleteFailureException.cs │ │ │ ├── NotFoundException.cs │ │ │ └── ValidationException.cs │ │ ├── Models │ │ │ ├── ErrorType.cs │ │ │ ├── Response.cs │ │ │ ├── MultiResponse.cs │ │ │ ├── User.cs │ │ │ ├── Result.cs │ │ │ ├── PaginationFilter.cs │ │ │ └── PagedResponse.cs │ │ ├── Interfaces │ │ │ ├── IEmailSender.cs │ │ │ ├── ICurrentUserService.cs │ │ │ ├── IResponseCacheService.cs │ │ │ ├── IAuctionSystemDbContext.cs │ │ │ └── IUserManager.cs │ │ ├── EmailSenderHelper.cs │ │ ├── Behaviours │ │ │ ├── RequestLogger.cs │ │ │ ├── RequestValidationBehaviour.cs │ │ │ └── RequestPerformanceBehaviour.cs │ │ └── Helpers │ │ │ └── PaginationHelper.cs │ │ ├── AppSettingsModels │ │ ├── SendGridOptions.cs │ │ ├── RedisCacheOptions.cs │ │ ├── JwtSettings.cs │ │ └── CloudinaryOptions.cs │ │ ├── Admin │ │ ├── Queries │ │ │ └── List │ │ │ │ ├── ListAllUsersQueryFilter.cs │ │ │ │ ├── ListAllUsersQuery.cs │ │ │ │ └── ListAllUsersResponseModel.cs │ │ └── Commands │ │ │ ├── CreateAdmin │ │ │ ├── CreateAdminCommand.cs │ │ │ ├── CreateAdminCommandValidator.cs │ │ │ └── CreateAdminCommandHandler.cs │ │ │ └── DeleteAdmin │ │ │ ├── DeleteAdminCommand.cs │ │ │ ├── DeleteAdminCommandValidator.cs │ │ │ └── DeleteAdminCommandHandler.cs │ │ ├── Users │ │ └── Commands │ │ │ ├── Logout │ │ │ ├── LogoutUserCommand.cs │ │ │ └── LogoutUserCommandHandler.cs │ │ │ ├── AuthSuccessResponse.cs │ │ │ ├── ConfirmEmail │ │ │ ├── ConfirmEmailCommand.cs │ │ │ ├── ConfirmEmailCommandValidator.cs │ │ │ └── ConfirmEmailCommandHandler.cs │ │ │ ├── CreateUser │ │ │ ├── CreateUserCommand.cs │ │ │ ├── CreateUserCommandValidator.cs │ │ │ └── CreateUserCommandHandler.cs │ │ │ ├── LoginUser │ │ │ ├── LoginUserCommand.cs │ │ │ ├── LoginUserCommandValidator.cs │ │ │ └── LoginUserCommandHandler.cs │ │ │ └── Jwt │ │ │ ├── Refresh │ │ │ └── JwtRefreshTokenCommand.cs │ │ │ ├── GenerateJwtTokenCommandValidator.cs │ │ │ ├── GenerateJwtTokenCommand.cs │ │ │ └── GenerateJwtTokenCommandHandler.cs │ │ ├── Categories │ │ └── Queries │ │ │ └── List │ │ │ ├── ListCategoriesQuery.cs │ │ │ ├── SubCategoriesDto.cs │ │ │ ├── ListCategoriesResponseModel.cs │ │ │ └── ListCategoriesQueryHandler.cs │ │ ├── Items │ │ ├── Commands │ │ │ ├── ItemResponseModel.cs │ │ │ ├── DeleteItem │ │ │ │ ├── DeleteItemCommand.cs │ │ │ │ ├── DeleteItemCommandValidator.cs │ │ │ │ └── DeleteItemCommandHandler.cs │ │ │ ├── UpdateItem │ │ │ │ ├── UpdateItemCommand.cs │ │ │ │ └── UpdateItemCommandValidator.cs │ │ │ └── CreateItem │ │ │ │ ├── CreateItemCommand.cs │ │ │ │ ├── CreateItemCommandValidator.cs │ │ │ │ └── CreateItemCommandHandler.cs │ │ └── Queries │ │ │ ├── List │ │ │ ├── ListItemsQuery.cs │ │ │ ├── ListAllItemsQueryFilter.cs │ │ │ └── ListItemsResponseModel.cs │ │ │ └── Details │ │ │ ├── GetItemDetailsQuery.cs │ │ │ ├── ItemDetailsResponseModel.cs │ │ │ └── GetItemDetailsQueryHandler.cs │ │ ├── Pictures │ │ ├── Commands │ │ │ ├── DeletePicture │ │ │ │ ├── DeletePictureCommand.cs │ │ │ │ └── DeletePictureCommandValidator.cs │ │ │ ├── CreatePicture │ │ │ │ ├── CreatePictureCommandValidator.cs │ │ │ │ └── CreatePictureCommand.cs │ │ │ └── UpdatePicture │ │ │ │ ├── UpdatePictureCommandValidator.cs │ │ │ │ └── UpdatePictureCommand.cs │ │ ├── PictureResponseModel.cs │ │ └── Queries │ │ │ ├── GetPictureDetailsQuery.cs │ │ │ ├── PictureDetailsResponseModel.cs │ │ │ └── GetPictureDetailsQueryHandler.cs │ │ ├── Notifications │ │ └── Models │ │ │ └── ItemDeletedNotification.cs │ │ ├── README.md │ │ ├── Bids │ │ ├── Commands │ │ │ └── CreateBid │ │ │ │ ├── CreateBidCommand.cs │ │ │ │ └── CreateBidCommandValidator.cs │ │ └── Queries │ │ │ └── Details │ │ │ ├── GetHighestBidDetailsQuery.cs │ │ │ ├── GetHighestBidDetailsResponseModel.cs │ │ │ └── GetHighestBidDetailsQueryHandler.cs │ │ ├── AppConstants.cs │ │ ├── DependencyInjection.cs │ │ ├── Application.csproj │ │ ├── SeedSampleData │ │ └── SeedSampleDataCommand.cs │ │ └── ExceptionMessages.cs └── Infrastructure │ ├── AuctionSystem.Infrastructure │ ├── README.md │ ├── MachineDateTime.cs │ ├── Identity │ │ ├── IdentityResultExtensions.cs │ │ └── FourDigitTokenProvider.cs │ ├── AuctionSystem.Infrastructure.csproj │ ├── DependencyInjection.cs │ └── EmailSender.cs │ └── Persistence │ ├── ConfigurationExtensions.cs │ ├── AuctionSystemDbContextFactory.cs │ ├── Configurations │ ├── RefreshTokenConfiguration.cs │ ├── PictureConfiguration.cs │ ├── BidConfiguration.cs │ ├── SubCategoryConfiguration.cs │ ├── CategoryConfiguration.cs │ ├── AuctionUserConfiguration.cs │ └── ItemConfiguration.cs │ ├── Migrations │ ├── 20200515095428_RemoveBidTableMadeOnColumn.cs │ └── 20200622181552_AddSetNullDeleteBehaviourForItemIdColumn.cs │ ├── Persistence.csproj │ └── DependencyInjection.cs ├── Tests ├── Api.IntegrationTests │ ├── UnitTest1.cs │ └── Api.IntegrationTests.csproj ├── Application.UnitTests │ ├── Mappings │ │ └── MappingTestsFixture.cs │ ├── Setup │ │ ├── CommandTestBase.cs │ │ ├── QueryTestFixture.cs │ │ ├── IdentityMocker.cs │ │ ├── TestSetup.cs │ │ └── DataConstants.cs │ ├── Categories │ │ └── Queries │ │ │ └── ListCategoriesQueryHandlerTests.cs │ ├── Application.UnitTests.csproj │ ├── Bids │ │ └── Queries │ │ │ └── GetHighestBidDetailsQueryHandlerTests.cs │ ├── Items │ │ └── Queries │ │ │ └── GetItemDetailsQueryHandlerTests.cs │ ├── Pictures │ │ └── Queries │ │ │ └── GetPictureDetailsQueryHandlerTests.cs │ └── Admin │ │ └── Queries │ │ └── ListAllUsersQueryHandlerTests.cs └── Persistence.IntegrationTests │ └── Persistence.IntegrationTests.csproj └── LICENSE /src/Presentation/SpaWeb/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/Core/Common/README.md: -------------------------------------------------------------------------------- 1 | # Common Layer 2 | 3 | This will contain all cross-cutting concerns and has no dependencies at all. -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/DateTimePicker/DateTimePicker.css: -------------------------------------------------------------------------------- 1 | .react-datepicker-wrapper { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Home/Header/Header.css: -------------------------------------------------------------------------------- 1 | .nav-item.dropdown:hover .dropdown-menu { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nirzaf/AuctionSystem/HEAD/src/Presentation/SpaWeb/public/favicon.ico -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nirzaf/AuctionSystem/HEAD/src/Presentation/SpaWeb/public/logo192.png -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nirzaf/AuctionSystem/HEAD/src/Presentation/SpaWeb/public/logo512.png -------------------------------------------------------------------------------- /src/Core/Common/AutoMapping/Interfaces/IMapWith.cs: -------------------------------------------------------------------------------- 1 | namespace Common.AutoMapping.Interfaces 2 | { 3 | // Marker interface 4 | public interface IMapWith { } 5 | } -------------------------------------------------------------------------------- /src/Core/Domain/README.md: -------------------------------------------------------------------------------- 1 | # Domain Layer 2 | 3 | This will contain all entities, enums, exceptions, types and logic specific to the domain and has no dependencies at all. 4 | -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Items/List/Search/Search.css: -------------------------------------------------------------------------------- 1 | .default-item-search { 2 | border: 1px solid #e3e6ef; 3 | background: #fff; 4 | padding: 2rem; 5 | } 6 | -------------------------------------------------------------------------------- /Tests/Api.IntegrationTests/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | namespace Api.IntegrationTests 2 | { 3 | public class UnitTest1 4 | { 5 | //TODO: Add integration tests for Api 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Items/Create/Create.css: -------------------------------------------------------------------------------- 1 | .valid-feedback { 2 | display: block !important; 3 | } 4 | 5 | .invalid-feedback { 6 | display: block !important; 7 | } 8 | -------------------------------------------------------------------------------- /src/Core/Application/Common/Exceptions/UnauthorizedException.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Exceptions 2 | { 3 | using System; 4 | 5 | public class UnauthorizedException : Exception { } 6 | } -------------------------------------------------------------------------------- /src/Core/Application/AppSettingsModels/SendGridOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Application.AppSettingsModels 2 | { 3 | public class SendGridOptions 4 | { 5 | public string SendGridApiKey { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/Models/ErrorType.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Models 2 | { 3 | public enum ErrorType 4 | { 5 | General, 6 | TokenExpired, 7 | AccountNotConfirmed, 8 | } 9 | } -------------------------------------------------------------------------------- /src/Core/Common/IDateTime.cs: -------------------------------------------------------------------------------- 1 | namespace Common 2 | { 3 | using System; 4 | 5 | public interface IDateTime 6 | { 7 | DateTime Now { get; } 8 | 9 | DateTime UtcNow { get; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Application/Admin/Queries/List/ListAllUsersQueryFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Admin.Queries.List 2 | { 3 | public class ListAllUsersQueryFilter 4 | { 5 | public string UserId { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /src/Presentation/Api/ApiConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Api 2 | { 3 | public static class ApiConstants 4 | { 5 | public const string RefreshToken = "refreshToken"; 6 | public const string JwtToken = "accessToken"; 7 | } 8 | } -------------------------------------------------------------------------------- /src/Core/Common/AutoMapping/Interfaces/IHaveCustomMapping.cs: -------------------------------------------------------------------------------- 1 | namespace Common.AutoMapping.Interfaces 2 | { 3 | using AutoMapper; 4 | 5 | public interface IHaveCustomMapping 6 | { 7 | void ConfigureMapping(Profile mapper); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Presentation/Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Core/Application/Users/Commands/Logout/LogoutUserCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Users.Commands.Logout 2 | { 3 | using MediatR; 4 | 5 | public class LogoutUserCommand : IRequest 6 | { 7 | public string RefreshToken { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Core/Application/AppSettingsModels/RedisCacheOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Application.AppSettingsModels 2 | { 3 | public class RedisCacheOptions 4 | { 5 | public bool Enabled { get; set; } 6 | 7 | public string ConnectionString { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Core/Application/Categories/Queries/List/ListCategoriesQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Categories.Queries.List 2 | { 3 | using Common.Models; 4 | using MediatR; 5 | 6 | public class ListCategoriesQuery : IRequest> { } 7 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/Models/Response.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Models 2 | { 3 | public class Response 4 | { 5 | public Response(T data) 6 | { 7 | this.Data = data; 8 | } 9 | 10 | public T Data { get; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Core/Application/AppSettingsModels/JwtSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Application.AppSettingsModels 2 | { 3 | using System; 4 | 5 | public class JwtSettings 6 | { 7 | public string Secret { get; set; } 8 | 9 | public TimeSpan TokenLifetime { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Application/Users/Commands/AuthSuccessResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Users.Commands 2 | { 3 | using System; 4 | 5 | public class AuthSuccessResponse 6 | { 7 | public string Token { get; set; } 8 | 9 | public Guid RefreshToken { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/Exceptions/BadRequestException.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Exceptions 2 | { 3 | using System; 4 | 5 | public class BadRequestException : Exception 6 | { 7 | public BadRequestException(string message) 8 | : base(message) { } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/Interfaces/IEmailSender.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Interfaces 2 | { 3 | using System.Threading.Tasks; 4 | 5 | public interface IEmailSender 6 | { 7 | Task SendEmailAsync(string sender, string receiver, string subject, string htmlMessage); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/services/categoriesService.js: -------------------------------------------------------------------------------- 1 | import api from "../utils/helpers/api"; 2 | 3 | const getAll = () => { 4 | return api 5 | .get(process.env.REACT_APP_API_CATEGORIES_ENDPOINT) 6 | .then((response) => response); 7 | }; 8 | 9 | export default { 10 | getAll, 11 | }; 12 | -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/utils/helpers/slug.js: -------------------------------------------------------------------------------- 1 | import slugify from "react-slugify"; 2 | 3 | export const itemDetailsSlug = (title, id) => { 4 | return `/items/${slugify(title)}/${id}`; 5 | }; 6 | 7 | export const itemEditSlug = (title, id) => { 8 | return `/items/edit/${slugify(title)}/${id}`; 9 | }; 10 | -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Error/Error.css: -------------------------------------------------------------------------------- 1 | .error-box { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | text-align: center; 7 | height: 80vh; 8 | } 9 | 10 | .error-box .error-heading { 11 | color: dimgray; 12 | font-size: 5em; 13 | } 14 | -------------------------------------------------------------------------------- /src/Core/Application/Admin/Commands/CreateAdmin/CreateAdminCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Admin.Commands.CreateAdmin 2 | { 3 | using MediatR; 4 | 5 | public class CreateAdminCommand : IRequest 6 | { 7 | public string Email { get; set; } 8 | 9 | public string Role { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Application/Admin/Commands/DeleteAdmin/DeleteAdminCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Admin.Commands.DeleteAdmin 2 | { 3 | using MediatR; 4 | 5 | public class DeleteAdminCommand : IRequest 6 | { 7 | public string Email { get; set; } 8 | 9 | public string Role { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Application/AppSettingsModels/CloudinaryOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Application.AppSettingsModels 2 | { 3 | public class CloudinaryOptions 4 | { 5 | public string CloudName { get; set; } 6 | 7 | public string ApiKey { get; set; } 8 | 9 | public string ApiSecret { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/services/bidService.js: -------------------------------------------------------------------------------- 1 | import api from "../utils/helpers/api"; 2 | 3 | const getHighestBid = (itemId) => { 4 | return api 5 | .get(`${process.env.REACT_APP_API_BIDS_ENDPOINT}/getHighestBid/${itemId}`) 6 | .then((response) => response); 7 | }; 8 | 9 | export default { getHighestBid }; 10 | -------------------------------------------------------------------------------- /src/Core/Application/Users/Commands/ConfirmEmail/ConfirmEmailCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Users.Commands.ConfirmEmail 2 | { 3 | using MediatR; 4 | 5 | public class ConfirmEmailCommand : IRequest 6 | { 7 | public string Code { get; set; } 8 | 9 | public string Email { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Infrastructure/AuctionSystem.Infrastructure/README.md: -------------------------------------------------------------------------------- 1 | # Infrastructure Layer 2 | 3 | This layer contains classes for accessing external resources such as file systems, web services, smtp, and so on. 4 | These classes should be based on interfaces defined within the application layer and ideally the only one dependency is Application layer -------------------------------------------------------------------------------- /src/Core/Application/Items/Commands/ItemResponseModel.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Items.Commands 2 | { 3 | using System; 4 | 5 | public class ItemResponseModel 6 | { 7 | public ItemResponseModel(Guid id) 8 | { 9 | this.Id = id; 10 | } 11 | 12 | public Guid Id { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Core/Domain/Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Presentation/Api/Models/Errors/ErrorModel.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models.Errors 2 | { 3 | public class ErrorModel 4 | { 5 | public string Title { get; set; } 6 | 7 | public int Status { get; set; } 8 | 9 | public string TraceId { get; set; } 10 | 11 | public string Error { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/services/picturesService.js: -------------------------------------------------------------------------------- 1 | const createImageObj = (pictures) => { 2 | let data = []; 3 | pictures.forEach((element) => { 4 | data.push({ 5 | original: element.url, 6 | thumbnail: element.url, 7 | }); 8 | }); 9 | 10 | return data; 11 | }; 12 | 13 | export default createImageObj; 14 | -------------------------------------------------------------------------------- /src/Core/Application/Items/Queries/List/ListItemsQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Items.Queries.List 2 | { 3 | using Common.Models; 4 | using MediatR; 5 | 6 | public class ListItemsQuery : PaginationFilter, IRequest> 7 | { 8 | public ListAllItemsQueryFilter Filters { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/BadRequestErrorModel.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples 2 | { 3 | using Models.Errors; 4 | 5 | // This class is created only to be visualized in the swagger ui. 6 | // When they add option to visualize proper models this can be removed 7 | public class BadRequestErrorModel : ErrorModel 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/NotFoundErrorModel.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples 2 | { 3 | using Models.Errors; 4 | 5 | // This class is created only to be visualized in the swagger ui. 6 | // When they add option to visualize proper models this can be removed 7 | public class NotFoundErrorModel : ErrorModel 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /src/Core/Application/Pictures/Commands/DeletePicture/DeletePictureCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Pictures.Commands.DeletePicture 2 | { 3 | using System; 4 | using MediatR; 5 | 6 | public class DeletePictureCommand : IRequest 7 | { 8 | public Guid PictureId { get; set; } 9 | 10 | public Guid ItemId { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Core/Application/Admin/Queries/List/ListAllUsersQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Admin.Queries.List 2 | { 3 | using Common.Models; 4 | using MediatR; 5 | 6 | public class ListAllUsersQuery : PaginationFilter, IRequest> 7 | { 8 | public ListAllUsersQueryFilter Filters { get; set; } = null; 9 | } 10 | } -------------------------------------------------------------------------------- /src/Core/Domain/Entities/Picture.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Entities 2 | { 3 | using System; 4 | using Common; 5 | 6 | public class Picture : AuditableEntity 7 | { 8 | public Guid Id { get; set; } 9 | public string Url { get; set; } 10 | 11 | public Guid ItemId { get; set; } 12 | public Item Item { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/Models/MultiResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Models 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class MultiResponse 6 | { 7 | public MultiResponse(IEnumerable data) 8 | { 9 | this.Data = data; 10 | } 11 | 12 | public IEnumerable Data { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Core/Application/Pictures/PictureResponseModel.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Pictures 2 | { 3 | using System; 4 | using Domain.Entities; 5 | using global::Common.AutoMapping.Interfaces; 6 | 7 | public class PictureResponseModel : IMapWith 8 | { 9 | public Guid Id { get; set; } 10 | 11 | public string Url { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Core/Application/Users/Commands/CreateUser/CreateUserCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Users.Commands.CreateUser 2 | { 3 | using MediatR; 4 | 5 | public class CreateUserCommand : IRequest 6 | { 7 | public string Email { get; set; } 8 | 9 | public string FullName { get; set; } 10 | 11 | public string Password { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Core/Application/Users/Commands/LoginUser/LoginUserCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Users.Commands.LoginUser 2 | { 3 | using Common.Models; 4 | using MediatR; 5 | 6 | public class LoginUserCommand : IRequest> 7 | { 8 | public string Email { get; set; } 9 | 10 | public string Password { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/Exceptions/DeleteFailureException.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Exceptions 2 | { 3 | using System; 4 | 5 | public class DeleteFailureException : Exception 6 | { 7 | public DeleteFailureException(string name, object key, string message) 8 | : base($"Deletion of entity \"{name}\" ({key}) failed. {message}") { } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/ConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Persistence 2 | { 3 | using Microsoft.Extensions.Configuration; 4 | 5 | public static class ConfigurationExtensions 6 | { 7 | public static string GetDefaultConnectionString(this IConfiguration configuration) 8 | => configuration.GetConnectionString("DefaultConnection"); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/Interfaces/ICurrentUserService.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Interfaces 2 | { 3 | public interface ICurrentUserService 4 | { 5 | string UserId { get; } 6 | 7 | bool IsAuthenticated { get; } 8 | 9 | //TODO: Don't trust claims... Check later in database if user is really admin 10 | bool IsAdmin { get; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Core/Application/Items/Commands/DeleteItem/DeleteItemCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Items.Commands.DeleteItem 2 | { 3 | using System; 4 | using MediatR; 5 | 6 | public class DeleteItemCommand : IRequest 7 | { 8 | public DeleteItemCommand(Guid id) 9 | { 10 | this.Id = id; 11 | } 12 | 13 | public Guid Id { get; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Presentation/Api/Models/UsersFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models 2 | { 3 | using Application.Admin.Queries.List; 4 | using global::Common.AutoMapping.Interfaces; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | public class UsersFilter : IMapWith 8 | { 9 | [FromQuery(Name = "userId")] 10 | public string UserId { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Core/Application/Categories/Queries/List/SubCategoriesDto.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Categories.Queries.List 2 | { 3 | using System; 4 | using Domain.Entities; 5 | using global::Common.AutoMapping.Interfaces; 6 | 7 | public class SubCategoriesDto : IMapWith 8 | { 9 | public Guid Id { get; set; } 10 | 11 | public string Name { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/Interfaces/IResponseCacheService.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Interfaces 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | public interface IResponseCacheService 7 | { 8 | Task CacheResponseAsync(string key, object response, TimeSpan cachingTime); 9 | 10 | Task GetCachedResponseAsync(string cacheKey); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Core/Common/Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Core/Application/Items/Commands/DeleteItem/DeleteItemCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Items.Commands.DeleteItem 2 | { 3 | using FluentValidation; 4 | 5 | public class DeleteItemCommandValidator : AbstractValidator 6 | { 7 | public DeleteItemCommandValidator() 8 | { 9 | this.RuleFor(p => p.Id).NotEmpty(); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Core/Domain/Common/AuditableEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Common 2 | { 3 | using System; 4 | 5 | public class AuditableEntity 6 | { 7 | public string CreatedBy { get; set; } 8 | 9 | public DateTime Created { get; set; } = DateTime.UtcNow; 10 | 11 | public string LastModifiedBy { get; set; } 12 | 13 | public DateTime? LastModified { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Infrastructure/AuctionSystem.Infrastructure/MachineDateTime.cs: -------------------------------------------------------------------------------- 1 | namespace AuctionSystem.Infrastructure 2 | { 3 | using System; 4 | using Common; 5 | 6 | public class MachineDateTime : IDateTime 7 | { 8 | public int CurrentYear => DateTime.Now.Year; 9 | 10 | public DateTime Now => DateTime.Now; 11 | 12 | public DateTime UtcNow => DateTime.UtcNow; 13 | } 14 | } -------------------------------------------------------------------------------- /src/Core/Application/Notifications/Models/ItemDeletedNotification.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Notifications.Models 2 | { 3 | using System; 4 | using MediatR; 5 | 6 | public class ItemDeletedNotification : INotification 7 | { 8 | public ItemDeletedNotification(Guid itemId) 9 | { 10 | this.ItemId = itemId; 11 | } 12 | 13 | public Guid ItemId { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Core/Application/Users/Commands/Jwt/Refresh/JwtRefreshTokenCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Users.Commands.Jwt.Refresh 2 | { 3 | using System; 4 | using Common.Models; 5 | using MediatR; 6 | 7 | public class JwtRefreshTokenCommand : IRequest> 8 | { 9 | public string Token { get; set; } 10 | 11 | public Guid RefreshToken { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Core/Domain/Entities/Category.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Entities 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Common; 6 | 7 | public class Category : AuditableEntity 8 | { 9 | public Guid Id { get; set; } 10 | public string Name { get; set; } 11 | 12 | public ICollection SubCategories { get; set; } = new HashSet(); 13 | } 14 | } -------------------------------------------------------------------------------- /src/Core/Application/Pictures/Commands/CreatePicture/CreatePictureCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Pictures.Commands.CreatePicture 2 | { 3 | using FluentValidation; 4 | 5 | public class CreatePictureCommandValidator : AbstractValidator 6 | { 7 | public CreatePictureCommandValidator() 8 | { 9 | this.RuleFor(p => p.ItemId).NotEmpty(); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Presentation/Api/Models/Errors/ValidationErrorModel.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models.Errors 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class ValidationErrorModel 6 | { 7 | public string Title { get; set; } 8 | 9 | public int Status { get; set; } 10 | 11 | public string TraceId { get; set; } 12 | 13 | public IDictionary Errors { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Core/Application/README.md: -------------------------------------------------------------------------------- 1 | # Application Layer 2 | 3 | This layer contains all application logic. It is dependent on the domain layer, but has no dependencies on any other layer or project. 4 | This layer defines interfaces that are implemented by outside layers. 5 | For example, if the application need to access a notification service, a new interface would be added to application and an implementation would be created within infrastructure. -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Home/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { Header } from "./Header/Header"; 3 | import { HottestItems } from "./HottestItems/HottestItems"; 4 | import { LiveItems } from "./LiveItems/LiveItems"; 5 | 6 | export const Home = () => { 7 | return ( 8 | 9 |
10 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/AuctionSystemDbContextFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Persistence 2 | { 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | public class AuctionSystemDbContextFactory : DesignTimeDbContextFactoryBase 6 | { 7 | protected override AuctionSystemDbContext CreateNewInstance(DbContextOptions options) 8 | => new AuctionSystemDbContext(options); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/Core/Application/Pictures/Queries/GetPictureDetailsQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Pictures.Queries 2 | { 3 | using System; 4 | using Common.Models; 5 | using MediatR; 6 | 7 | public class GetPictureDetailsQuery : IRequest> 8 | { 9 | public GetPictureDetailsQuery(Guid id) 10 | { 11 | this.Id = id; 12 | } 13 | 14 | public Guid Id { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Core/Domain/Entities/AuctionUser.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Entities 2 | { 3 | using System.Collections.Generic; 4 | using Microsoft.AspNetCore.Identity; 5 | 6 | public class AuctionUser : IdentityUser 7 | { 8 | public string FullName { get; set; } 9 | 10 | public ICollection ItemsSold { get; set; } = new HashSet(); 11 | 12 | public ICollection Bids { get; set; } = new HashSet(); 13 | } 14 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/Core/Application/Items/Queries/Details/GetItemDetailsQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Items.Queries.Details 2 | { 3 | using System; 4 | using Common.Models; 5 | using MediatR; 6 | 7 | public class GetItemDetailsQuery : IRequest> 8 | { 9 | public GetItemDetailsQuery(Guid id) 10 | { 11 | this.Id = id; 12 | } 13 | 14 | public Guid Id { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Core/Application/Pictures/Commands/UpdatePicture/UpdatePictureCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Pictures.Commands.UpdatePicture 2 | { 3 | using FluentValidation; 4 | using Items.Commands.UpdateItem; 5 | 6 | public class UpdatePictureCommandValidator : AbstractValidator 7 | { 8 | public UpdatePictureCommandValidator() 9 | { 10 | this.RuleFor(p => p.Id).NotEmpty(); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Core/Domain/Entities/Bid.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Entities 2 | { 3 | using System; 4 | using Common; 5 | 6 | public class Bid : AuditableEntity 7 | { 8 | public Guid Id { get; set; } 9 | public decimal Amount { get; set; } 10 | 11 | public string UserId { get; set; } 12 | public AuctionUser User { get; set; } 13 | 14 | public Guid? ItemId { get; set; } 15 | public Item Item { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Core/Application/Users/Commands/Jwt/GenerateJwtTokenCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Users.Commands.Jwt 2 | { 3 | using FluentValidation; 4 | 5 | public class GenerateJwtTokenCommandValidator : AbstractValidator 6 | { 7 | public GenerateJwtTokenCommandValidator() 8 | { 9 | this.RuleFor(p => p.UserId).NotEmpty(); 10 | this.RuleFor(p => p.Username).NotEmpty(); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Requests/Users/LoginUserRequestExample.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Requests.Users 2 | { 3 | using Application.Users.Commands.LoginUser; 4 | using Swashbuckle.AspNetCore.Filters; 5 | 6 | public class LoginUserRequestExample : IExamplesProvider 7 | { 8 | public LoginUserCommand GetExamples() 9 | => new LoginUserCommand { Email = "test@test.com", Password = "test123" }; 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/Exceptions/NotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Exceptions 2 | { 3 | using System; 4 | 5 | public class NotFoundException : Exception 6 | { 7 | public NotFoundException(string name) 8 | : base($"Such '{name}' was not found.") { } 9 | 10 | //public NotFoundException(string name, object key) 11 | // : base($"{name} with Id ({key}) was not found.") 12 | //{ 13 | //} 14 | } 15 | } -------------------------------------------------------------------------------- /src/Core/Application/Bids/Commands/CreateBid/CreateBidCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Bids.Commands.CreateBid 2 | { 3 | using System; 4 | using Domain.Entities; 5 | using global::Common.AutoMapping.Interfaces; 6 | using MediatR; 7 | 8 | public class CreateBidCommand : IRequest, IMapWith 9 | { 10 | public decimal Amount { get; set; } 11 | 12 | public Guid ItemId { get; set; } 13 | 14 | public string UserId { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Core/Application/Pictures/Commands/DeletePicture/DeletePictureCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Pictures.Commands.DeletePicture 2 | { 3 | using FluentValidation; 4 | 5 | public class DeletePictureCommandValidator : AbstractValidator 6 | { 7 | public DeletePictureCommandValidator() 8 | { 9 | this.RuleFor(p => p.PictureId).NotEmpty(); 10 | this.RuleFor(p => p.ItemId).NotEmpty(); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Requests/Admin/CreateAdminRequestExample.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Requests.Admin 2 | { 3 | using Application.Admin.Commands.CreateAdmin; 4 | using Swashbuckle.AspNetCore.Filters; 5 | 6 | public class CreateAdminRequestExample : IExamplesProvider 7 | { 8 | public CreateAdminCommand GetExamples() 9 | => new CreateAdminCommand { Email = "test1@test.com", Role = "Administrator" }; 10 | } 11 | } -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Requests/Admin/DeleteAdminRequestExample.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Requests.Admin 2 | { 3 | using Application.Admin.Commands.DeleteAdmin; 4 | using Swashbuckle.AspNetCore.Filters; 5 | 6 | public class DeleteAdminRequestExample : IExamplesProvider 7 | { 8 | public DeleteAdminCommand GetExamples() 9 | => new DeleteAdminCommand { Email = "admin@admin.com", Role = "Administrator" }; 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Application/Bids/Queries/Details/GetHighestBidDetailsQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Bids.Queries.Details 2 | { 3 | using System; 4 | using Common.Models; 5 | using MediatR; 6 | 7 | public class GetHighestBidDetailsQuery : IRequest> 8 | { 9 | public GetHighestBidDetailsQuery(Guid itemId) 10 | { 11 | this.ItemId = itemId; 12 | } 13 | 14 | public Guid ItemId { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Core/Application/Pictures/Queries/PictureDetailsResponseModel.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Pictures.Queries 2 | { 3 | using System; 4 | using Domain.Entities; 5 | using global::Common.AutoMapping.Interfaces; 6 | 7 | public class PictureDetailsResponseModel : IMapWith 8 | { 9 | public Guid Id { get; set; } 10 | 11 | public string Url { get; set; } 12 | 13 | public Guid ItemId { get; set; } 14 | 15 | public string ItemUserId { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Core/Domain/Entities/SubCategory.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Entities 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Common; 6 | 7 | public class SubCategory : AuditableEntity 8 | { 9 | public Guid Id { get; set; } 10 | public string Name { get; set; } 11 | 12 | public Guid CategoryId { get; set; } 13 | public Category Category { get; set; } 14 | 15 | public ICollection Items { get; set; } = new HashSet(); 16 | } 17 | } -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Requests/Users/CreateUserRequestExample.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Requests.Users 2 | { 3 | using Application.Users.Commands.CreateUser; 4 | using Swashbuckle.AspNetCore.Filters; 5 | 6 | public class CreateUserRequestExample : IExamplesProvider 7 | { 8 | public CreateUserCommand GetExamples() 9 | => new CreateUserCommand { Email = "test@test.com", FullName = "Melik Pehlivanov", Password = "Test123" }; 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Application/Users/Commands/Jwt/GenerateJwtTokenCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Users.Commands.Jwt 2 | { 3 | using MediatR; 4 | 5 | public class GenerateJwtTokenCommand : IRequest 6 | { 7 | public GenerateJwtTokenCommand(string userId, string username) 8 | { 9 | this.UserId = userId; 10 | this.Username = username; 11 | } 12 | 13 | public string UserId { get; } 14 | 15 | public string Username { get; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/utils/hooks/useTime.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import moment from "moment"; 3 | 4 | export const useTime = (interval = 60000) => { 5 | const [currentTime, setCurrentTime] = useState(moment().toDate()); 6 | 7 | useEffect(() => { 8 | const timer = setInterval(() => { 9 | setCurrentTime(moment().toDate()); 10 | }, interval); 11 | 12 | return () => clearInterval(timer); 13 | }, [currentTime, interval]); 14 | 15 | return { currentTime }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/Core/Application/Bids/Queries/Details/GetHighestBidDetailsResponseModel.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Bids.Queries.Details 2 | { 3 | using System; 4 | using Domain.Entities; 5 | using global::Common.AutoMapping.Interfaces; 6 | 7 | public class GetHighestBidDetailsResponseModel : IMapWith 8 | { 9 | public Guid Id { get; set; } 10 | 11 | public decimal Amount { get; set; } 12 | 13 | public string UserId { get; set; } 14 | 15 | public Guid ItemId { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Core/Application/Users/Commands/LoginUser/LoginUserCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Users.Commands.LoginUser 2 | { 3 | using FluentValidation; 4 | using global::Common; 5 | 6 | public class LoginUserCommandValidator : AbstractValidator 7 | { 8 | public LoginUserCommandValidator() 9 | { 10 | this.RuleFor(p => p.Email).NotEmpty().Matches(ModelConstants.User.EmailRegex); 11 | this.RuleFor(p => p.Password).NotEmpty(); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Requests/Bids/CreateBidRequestExample.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Requests.Bids 2 | { 3 | using System; 4 | using Application.Bids.Commands.CreateBid; 5 | using Swashbuckle.AspNetCore.Filters; 6 | 7 | public class CreateBidRequestExample : IExamplesProvider 8 | { 9 | public CreateBidCommand GetExamples() 10 | => new CreateBidCommand { Amount = 100000.99m, ItemId = Guid.NewGuid(), UserId = Guid.NewGuid().ToString() }; 11 | } 12 | } -------------------------------------------------------------------------------- /src/Core/Application/Admin/Commands/CreateAdmin/CreateAdminCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Admin.Commands.CreateAdmin 2 | { 3 | using FluentValidation; 4 | using global::Common; 5 | 6 | public class CreateAdminCommandValidator : AbstractValidator 7 | { 8 | public CreateAdminCommandValidator() 9 | { 10 | this.RuleFor(u => u.Email).NotEmpty().Matches(ModelConstants.User.EmailRegex); 11 | this.RuleFor(u => u.Role).NotEmpty(); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Core/Application/Admin/Commands/DeleteAdmin/DeleteAdminCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Admin.Commands.DeleteAdmin 2 | { 3 | using FluentValidation; 4 | using global::Common; 5 | 6 | public class DeleteAdminCommandValidator : AbstractValidator 7 | { 8 | public DeleteAdminCommandValidator() 9 | { 10 | this.RuleFor(u => u.Email).NotEmpty().Matches(ModelConstants.User.EmailRegex); 11 | this.RuleFor(u => u.Role).NotEmpty(); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Requests/Pictures/DeletePictureRequestExample.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Requests.Pictures 2 | { 3 | using System; 4 | using Application.Pictures.Commands.DeletePicture; 5 | using Swashbuckle.AspNetCore.Filters; 6 | 7 | public class DeletePictureRequestExample : IExamplesProvider 8 | { 9 | public DeletePictureCommand GetExamples() 10 | => new DeletePictureCommand { PictureId = Guid.NewGuid(), ItemId = Guid.NewGuid() }; 11 | } 12 | } -------------------------------------------------------------------------------- /src/Core/Application/Users/Commands/ConfirmEmail/ConfirmEmailCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Users.Commands.ConfirmEmail 2 | { 3 | using FluentValidation; 4 | using global::Common; 5 | 6 | public class ConfirmEmailCommandValidator : AbstractValidator 7 | { 8 | public ConfirmEmailCommandValidator() 9 | { 10 | this.RuleFor(p => p.Code).NotEmpty(); 11 | this.RuleFor(p => p.Email).NotEmpty().Matches(ModelConstants.User.EmailRegex); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Core/Application/Categories/Queries/List/ListCategoriesResponseModel.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Categories.Queries.List 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Domain.Entities; 6 | using global::Common.AutoMapping.Interfaces; 7 | 8 | public class ListCategoriesResponseModel : IMapWith 9 | { 10 | public Guid Id { get; set; } 11 | 12 | public string Name { get; set; } 13 | 14 | public IEnumerable SubCategories { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Core/Domain/Entities/RefreshToken.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Entities 2 | { 3 | using System; 4 | 5 | public class RefreshToken 6 | { 7 | public Guid Token { get; set; } 8 | public string JwtId { get; set; } 9 | public DateTime CreationDate { get; set; } 10 | public DateTime ExpiryDate { get; set; } 11 | public bool Used { get; set; } 12 | public bool Invalidated { get; set; } 13 | 14 | public string UserId { get; set; } 15 | public AuctionUser User { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Core/Application/Pictures/Commands/CreatePicture/CreatePictureCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Pictures.Commands.CreatePicture 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Common.Models; 6 | using MediatR; 7 | using Microsoft.AspNetCore.Http; 8 | 9 | public class CreatePictureCommand : IRequest> 10 | { 11 | public Guid ItemId { get; set; } 12 | 13 | public ICollection Pictures { get; set; } = new HashSet(); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Presentation/Api/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Controllers 2 | { 3 | using MediatR; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | [ApiController] 8 | [Produces("application/json")] 9 | [Route("api/[controller]")] 10 | public abstract class BaseController : ControllerBase 11 | { 12 | private IMediator _mediator; 13 | 14 | protected IMediator Mediator => this._mediator ??= this.HttpContext.RequestServices.GetService(); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Presentation/Api/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Api 2 | { 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Hosting; 5 | 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | private static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); 16 | } 17 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Home/HottestItems/HottestItems.css: -------------------------------------------------------------------------------- 1 | .react-card-carousel-container { 2 | position: relative; 3 | height: 23em; 4 | width: 100%; 5 | display: flex; 6 | flex: 1; 7 | justify-content: center; 8 | align-items: center; 9 | } 10 | 11 | .react-card { 12 | width: 150px; 13 | text-align: center; 14 | background: #52c0f5; 15 | color: #fff; 16 | border-radius: 10px; 17 | } 18 | 19 | @media only screen and (max-width: 1140px) { 20 | .react-card-carousel-container { 21 | height: 20em; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Responses/Items/SuccessfulItemCreateResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Responses.Items 2 | { 3 | using System; 4 | using Application.Common.Models; 5 | using Application.Items.Commands; 6 | using Swashbuckle.AspNetCore.Filters; 7 | 8 | public class SuccessfulItemCreateResponse : IExamplesProvider> 9 | { 10 | public Response GetExamples() 11 | => new Response(new ItemResponseModel(Guid.NewGuid())); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Infrastructure/AuctionSystem.Infrastructure/Identity/IdentityResultExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace AuctionSystem.Infrastructure.Identity 2 | { 3 | using System.Linq; 4 | using Application.Common.Models; 5 | using Microsoft.AspNetCore.Identity; 6 | 7 | public static class IdentityResultExtensions 8 | { 9 | public static Result ToApplicationResult(this IdentityResult result) 10 | => result.Succeeded 11 | ? Result.Success() 12 | : Result.Failure(result.Errors.Select(e => e.Description).First()); 13 | } 14 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/EmailSenderHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common 2 | { 3 | using System.Threading.Tasks; 4 | using Application; 5 | using Interfaces; 6 | 7 | public static class EmailSenderHelper 8 | { 9 | public static async Task SendConfirmationEmail(this IEmailSender emailSender, string email, string token) 10 | { 11 | await emailSender.SendEmailAsync(AppConstants.AppMainEmailAddress, email, "Verification code", 12 | $"Thanks for registering. Your verification code is {token}."); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Core/Application/Pictures/Commands/UpdatePicture/UpdatePictureCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Pictures.Commands.UpdatePicture 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using MediatR; 6 | using Microsoft.AspNetCore.Http; 7 | 8 | public class UpdatePictureCommand : IRequest 9 | { 10 | public Guid ItemId { get; set; } 11 | 12 | public ICollection PicturesToAdd { get; set; } = new HashSet(); 13 | 14 | public ICollection PicturesToRemove { get; set; } = new HashSet(); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Items/List/Container/Container.css: -------------------------------------------------------------------------------- 1 | .floating-card { 2 | transition-duration: 300ms; 3 | } 4 | 5 | .floating-card:hover { 6 | -webkit-box-shadow: -1px 9px 40px -12px rgba(0, 0, 0, 0.75) !important; 7 | -moz-box-shadow: -1px 9px 40px -12px rgba(0, 0, 0, 0.75) !important; 8 | box-shadow: -1px 9px 40px -12px rgba(0, 0, 0, 0.75) !important; 9 | -moz-transform: translate(0, -5px); 10 | -ms-transform: translate(0, -5px); 11 | -o-transform: translate(0, -5px); 12 | -webkit-transform: translate(0, -5px); 13 | transform: translate(0, -5px); 14 | } 15 | -------------------------------------------------------------------------------- /src/Presentation/Api/Extensions/ApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Extensions 2 | { 3 | using Microsoft.AspNetCore.Builder; 4 | 5 | public static class ApplicationBuilderExtensions 6 | { 7 | public static IApplicationBuilder UseSwaggerUi(this IApplicationBuilder app) 8 | => app 9 | .UseSwagger() 10 | .UseSwaggerUI(options => 11 | { 12 | options.SwaggerEndpoint("/swagger/v1/swagger.json", "AuctionSystem API"); 13 | options.RoutePrefix = string.Empty; 14 | }); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Responses/NotFoundResponseModel.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Responses 2 | { 3 | using Swashbuckle.AspNetCore.Filters; 4 | 5 | public class NotFoundResponseModel : IExamplesProvider 6 | { 7 | public NotFoundErrorModel GetExamples() 8 | => new NotFoundErrorModel 9 | { 10 | Error = "Such entity does not exist or this field could be empty", 11 | Status = 404, 12 | Title = "NotFound", 13 | TraceId = "9000006b-0407-fb00-b63f-84710c8421cb" 14 | }; 15 | } 16 | } -------------------------------------------------------------------------------- /src/Core/Application/Admin/Queries/List/ListAllUsersResponseModel.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Admin.Queries.List 2 | { 3 | using System.Collections.Generic; 4 | using Domain.Entities; 5 | using global::Common.AutoMapping.Interfaces; 6 | 7 | public class ListAllUsersResponseModel : IMapWith 8 | { 9 | public string Id { get; set; } 10 | 11 | public string Email { get; set; } 12 | 13 | public string FullName { get; set; } 14 | 15 | public ICollection CurrentRoles { get; set; } 16 | 17 | public ICollection NonCurrentRoles { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /Tests/Application.UnitTests/Mappings/MappingTestsFixture.cs: -------------------------------------------------------------------------------- 1 | namespace Application.UnitTests.Mappings 2 | { 3 | using AutoMapper; 4 | using global::Common.AutoMapping.Profiles; 5 | 6 | public class MappingTestsFixture 7 | { 8 | public MappingTestsFixture() 9 | { 10 | this.ConfigurationProvider = new MapperConfiguration(cfg => { cfg.AddProfile(); }); 11 | 12 | this.Mapper = this.ConfigurationProvider.CreateMapper(); 13 | } 14 | 15 | public IConfigurationProvider ConfigurationProvider { get; } 16 | 17 | public IMapper Mapper { get; } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Core/Application/Bids/Commands/CreateBid/CreateBidCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Bids.Commands.CreateBid 2 | { 3 | using FluentValidation; 4 | using global::Common; 5 | 6 | public class CreateBidCommandValidator : AbstractValidator 7 | { 8 | public CreateBidCommandValidator() 9 | { 10 | this.RuleFor(p => p.Amount).NotEmpty() 11 | .InclusiveBetween(ModelConstants.Bid.MinAmount, ModelConstants.Bid.MaxAmount); 12 | this.RuleFor(p => p.ItemId).NotEmpty(); 13 | this.RuleFor(p => p.UserId).NotEmpty(); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/Core/Application/Users/Commands/CreateUser/CreateUserCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Users.Commands.CreateUser 2 | { 3 | using FluentValidation; 4 | using global::Common; 5 | 6 | public class CreateUserCommandValidator : AbstractValidator 7 | { 8 | public CreateUserCommandValidator() 9 | { 10 | this.RuleFor(p => p.Email).NotEmpty().Matches(ModelConstants.User.EmailRegex); 11 | this.RuleFor(p => p.FullName).NotEmpty().MaximumLength(ModelConstants.User.FullNameMaxLength); 12 | this.RuleFor(p => p.Password).NotEmpty(); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Presentation/Api/Models/PaginationQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | using Application; 5 | using Application.Common.Models; 6 | using global::Common.AutoMapping.Interfaces; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | public class PaginationQuery : IMapWith 10 | { 11 | [FromQuery(Name = "pageNumber")] 12 | [Range(1, int.MaxValue)] 13 | public int PageNumber { get; set; } = 1; 14 | 15 | [FromQuery(Name = "pageSize")] 16 | [Range(1, int.MaxValue)] 17 | public int PageSize { get; set; } = AppConstants.PageSize; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Responses/BadRequestResponseModel.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Responses 2 | { 3 | using Swashbuckle.AspNetCore.Filters; 4 | 5 | public class BadRequestResponseModel : IExamplesProvider 6 | { 7 | public BadRequestErrorModel GetExamples() 8 | => new BadRequestErrorModel 9 | { 10 | Error = "An error occured while creating/deleting/updating given entity(User, Item and etc.)", 11 | Title = "BadRequest", 12 | Status = 400, 13 | TraceId = "8000006c-0007-ff00-b63f-84710c7967bb" 14 | }; 15 | } 16 | } -------------------------------------------------------------------------------- /Tests/Application.UnitTests/Setup/CommandTestBase.cs: -------------------------------------------------------------------------------- 1 | namespace Application.UnitTests.Setup 2 | { 3 | using System; 4 | using AutoMapper; 5 | using Persistence; 6 | 7 | public class CommandTestBase : IDisposable 8 | { 9 | public CommandTestBase() 10 | { 11 | this.Context = AuctionSystemContextFactory.Create(); 12 | 13 | this.Mapper = TestSetup.InitializeMapper(); 14 | } 15 | 16 | protected AuctionSystemDbContext Context { get; } 17 | 18 | protected IMapper Mapper { get; } 19 | 20 | public void Dispose() 21 | => AuctionSystemContextFactory.Destroy(this.Context); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Core/Application/AppConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Application 2 | { 3 | public static class AppConstants 4 | { 5 | public const string AppMainEmailAddress = "mytestedauctionsystem01@gmail.com"; 6 | public const string CategoriesPath = "../../Core/Application/SeedSampleData/Resources/categories.json"; 7 | public const string AdministratorRole = "Administrator"; 8 | 9 | public const string DefaultPictureUrl = 10 | "https://res.cloudinary.com/auctionsystem/image/upload/v1547833155/default-img.jpg"; 11 | 12 | public const int PageSize = 24; 13 | 14 | public const int RefreshTokenExpirationTimeInMonths = 6; 15 | } 16 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Home/LiveItems/Pictures/PictureContainer.css: -------------------------------------------------------------------------------- 1 | .picture-container { 2 | height: 15rem; 3 | } 4 | 5 | .card-img-listing { 6 | width: 100%; 7 | height: 100%; 8 | object-fit: cover; 9 | } 10 | 11 | .primary-picture { 12 | float: left; 13 | overflow: hidden; 14 | height: 100%; 15 | max-width: calc((100% / 3) * 2); 16 | text-align: center; 17 | display: block; 18 | border-right: 1px solid grey; 19 | position: relative; 20 | } 21 | 22 | .secondary-picture { 23 | position: relative; 24 | overflow: hidden; 25 | height: 50%; 26 | max-width: calc(100% / 3); 27 | display: inline-block; 28 | border-bottom: 1px solid grey; 29 | } 30 | -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Requests/Pictures/CreatePictureRequestExample.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Requests.Pictures 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Application.Pictures.Commands.CreatePicture; 6 | using Microsoft.AspNetCore.Http; 7 | using Swashbuckle.AspNetCore.Filters; 8 | 9 | public class CreatePictureRequestExample : IExamplesProvider 10 | { 11 | public CreatePictureCommand GetExamples() 12 | => new CreatePictureCommand 13 | { 14 | ItemId = Guid.NewGuid(), 15 | Pictures = new List() 16 | }; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Core/Application/Items/Queries/List/ListAllItemsQueryFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Items.Queries.List 2 | { 3 | using System; 4 | 5 | public class ListAllItemsQueryFilter 6 | { 7 | public string Title { get; set; } 8 | 9 | public string UserId { get; set; } 10 | 11 | public decimal? MinPrice { get; set; } 12 | 13 | public decimal? MaxPrice { get; set; } 14 | 15 | public DateTime? StartTime { get; set; } 16 | 17 | public DateTime? EndTime { get; set; } 18 | 19 | public bool GetLiveItems { get; set; } 20 | 21 | public int? MinimumPicturesCount { get; set; } 22 | 23 | public Guid SubCategoryId { get; set; } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/Models/User.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Models 2 | { 3 | using System; 4 | 5 | public class User 6 | { 7 | public string Id { get; set; } 8 | 9 | public string UserName { get; set; } 10 | 11 | public string FullName { get; set; } 12 | 13 | public string Email { get; set; } 14 | 15 | public bool IsEmailConfirmed { get; set; } 16 | 17 | public string PhoneNumber { get; set; } 18 | 19 | public bool PhoneNumberConfirmed { get; set; } 20 | 21 | public bool TwoFactorEnabled { get; set; } 22 | 23 | public DateTimeOffset? LockoutEnd { get; set; } 24 | 25 | public int AccessFailedCount { get; set; } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Persistence.Configurations 2 | { 3 | using Domain.Entities; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | public class RefreshTokenConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder 12 | .HasKey(p => p.Token); 13 | 14 | builder 15 | .Property(p => p.JwtId) 16 | .IsRequired(); 17 | 18 | builder 19 | .Property(p => p.UserId) 20 | .IsRequired(); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Presentation/Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=.;Database=AuctionSystemDb;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | }, 5 | "JwtSettings": { 6 | "Secret": "Put your app secret here", 7 | "TokenLifetime": "00:05:00" 8 | }, 9 | "RedisCacheSettings": { 10 | "Enabled": false, 11 | "ConnectionString": "localhost" 12 | }, 13 | "Cloudinary": { 14 | "CloudName": "Cloudinary Cloud name", 15 | "ApiKey": "Cloudinary Api Key", 16 | "ApiSecret": "Cloudinary Api Secret" 17 | }, 18 | "SendGrid": { 19 | "ApiKey": "SendGrid Api Key" 20 | }, 21 | "Logging": { 22 | "LogLevel": { 23 | "Default": "Warning" 24 | } 25 | }, 26 | "AllowedHosts": "*" 27 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/utils/helpers/localStorage.js: -------------------------------------------------------------------------------- 1 | const user = "user"; 2 | 3 | export const setUserInLocalStorage = (response) => { 4 | const data = response.data.data; 5 | 6 | const token = data.token; 7 | const jwtParams = JSON.parse(atob(token.split(".")[1])); 8 | const id = jwtParams.id; 9 | const isAdmin = jwtParams.role?.toLowerCase().includes("admin"); 10 | 11 | let dataToStore = {}; 12 | dataToStore.id = id; 13 | dataToStore.isAdmin = isAdmin ?? false; 14 | 15 | localStorage.setItem(user, JSON.stringify(dataToStore)); 16 | return dataToStore; 17 | }; 18 | 19 | export const removeUserFromLocalStorage = () => localStorage.removeItem(user); 20 | 21 | export const getUserFromLocalStorage = () => 22 | JSON.parse(localStorage.getItem(user)); 23 | -------------------------------------------------------------------------------- /src/Infrastructure/AuctionSystem.Infrastructure/AuctionSystem.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL = https://localhost:5001/api 2 | REACT_APP_CURRENCY_SIGN = € 3 | REACT_APP_API_LOGIN_ENDPOINT = /identity/login 4 | REACT_APP_API_REGISTER_ENDPOINT = /identity/register 5 | REACT_APP_API_CONFIRM_ACCOUNT_ENDPOINT = /identity/confirm 6 | REACT_APP_API_REFRESH_TOKENS_ENDPOINT = /identity/refresh 7 | REACT_APP_API_LOGOUT_ENDPOINT = /identity/logout 8 | REACT_APP_API_CATEGORIES_ENDPOINT = /categories 9 | REACT_APP_API_ITEMS_ENDPOINT = /items 10 | REACT_APP_API_BIDS_ENDPOINT = /bids 11 | REACT_APP_API_PICTURES_ENDPOINT = /pictures 12 | REACT_APP_API_ADMINISTRATION_ENDPOINT = /admin 13 | REACT_APP_DEFAULT_PICTURE_ID = defaultPicture 14 | REACT_APP_DEFAULT_PICTURE_URL = https://res.cloudinary.com/auctionsystem/image/upload/v1547833155/default-img.jpg -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Error/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { faSearch } from "@fortawesome/free-solid-svg-icons"; 4 | import { Link } from "react-router-dom"; 5 | 6 | export const NotFound = () => { 7 | return ( 8 |
9 | 10 |

404

11 |
The page you were looking for could not be found
12 | 13 | ...either that, or our server is on fire... 14 | 15 | 16 | Home 17 | 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import "bootstrap/dist/css/bootstrap.min.css"; 5 | import App from "./App"; 6 | import * as serviceWorker from "./serviceWorker"; 7 | import { createBrowserHistory } from "history"; 8 | import { Router } from "react-router-dom"; 9 | 10 | export const history = createBrowserHistory(); 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | document.getElementById("root") 16 | ); 17 | 18 | // If you want your app to work offline and load faster, you can change 19 | // unregister() to register() below. Note this comes with some pitfalls. 20 | // Learn more about service workers: https://bit.ly/CRA-PWA 21 | serviceWorker.unregister(); 22 | -------------------------------------------------------------------------------- /src/Core/Application/Common/Interfaces/IAuctionSystemDbContext.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Interfaces 2 | { 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Domain.Entities; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | public interface IAuctionSystemDbContext 9 | { 10 | DbSet Categories { get; set; } 11 | 12 | DbSet SubCategories { get; set; } 13 | 14 | DbSet Items { get; set; } 15 | 16 | DbSet Bids { get; set; } 17 | 18 | DbSet Pictures { get; set; } 19 | 20 | DbSet Users { get; set; } 21 | 22 | DbSet RefreshTokens { get; set; } 23 | 24 | Task SaveChangesAsync(CancellationToken cancellationToken); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Configurations/PictureConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Persistence.Configurations 2 | { 3 | using Domain.Entities; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | public class PictureConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder 12 | .ToTable("Pictures"); 13 | 14 | builder 15 | .HasKey(p => p.Id); 16 | 17 | builder 18 | .Property(p => p.Url) 19 | .IsRequired(); 20 | 21 | builder 22 | .Property(p => p.ItemId) 23 | .IsRequired(); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/Infrastructure/AuctionSystem.Infrastructure/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | namespace AuctionSystem.Infrastructure 2 | { 3 | using Application.Common.Interfaces; 4 | using Common; 5 | using Identity; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | public static class DependencyInjection 10 | { 11 | public static IServiceCollection AddInfrastructure(this IServiceCollection services, 12 | IConfiguration configuration) 13 | { 14 | services 15 | .AddScoped() 16 | .AddTransient() 17 | .AddTransient(); 18 | 19 | return services; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/services/adminService.js: -------------------------------------------------------------------------------- 1 | import api from "../utils/helpers/api"; 2 | 3 | const getUsers = (query) => { 4 | return api 5 | .get(process.env.REACT_APP_API_ADMINISTRATION_ENDPOINT, { params: query }) 6 | .then((response) => response); 7 | }; 8 | 9 | const addToRole = (email, role) => { 10 | return api 11 | .post(process.env.REACT_APP_API_ADMINISTRATION_ENDPOINT, { 12 | email, 13 | role, 14 | }) 15 | .then((response) => response); 16 | }; 17 | 18 | const removeFromRole = (email, role) => { 19 | return api 20 | .delete(process.env.REACT_APP_API_ADMINISTRATION_ENDPOINT, { 21 | data: { email, role }, 22 | }) 23 | .then((response) => response); 24 | }; 25 | 26 | export default { 27 | getUsers, 28 | addToRole, 29 | removeFromRole, 30 | }; 31 | -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Responses/Pictures/GetPictureDetailsResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Responses.Pictures 2 | { 3 | using System; 4 | using Application.Common.Models; 5 | using Application.Pictures.Queries; 6 | using Swashbuckle.AspNetCore.Filters; 7 | 8 | public class GetPictureDetailsResponse : IExamplesProvider> 9 | { 10 | public Response GetExamples() 11 | => new Response(new PictureDetailsResponseModel 12 | { 13 | Id = Guid.NewGuid(), 14 | ItemId = Guid.NewGuid(), 15 | Url = "https://google.com", 16 | ItemUserId = Guid.NewGuid().ToString(), 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/Application.UnitTests/Setup/QueryTestFixture.cs: -------------------------------------------------------------------------------- 1 | namespace Application.UnitTests.Setup 2 | { 3 | using System; 4 | using AutoMapper; 5 | using Persistence; 6 | using Xunit; 7 | 8 | public class QueryTestFixture : IDisposable 9 | { 10 | public QueryTestFixture() 11 | { 12 | this.Context = AuctionSystemContextFactory.Create(); 13 | 14 | this.Mapper = TestSetup.InitializeMapper(); 15 | } 16 | 17 | public AuctionSystemDbContext Context { get; } 18 | 19 | public IMapper Mapper { get; } 20 | 21 | public void Dispose() 22 | => AuctionSystemContextFactory.Destroy(this.Context); 23 | } 24 | 25 | [CollectionDefinition("QueryCollection")] 26 | public class QueryCollection : ICollectionFixture 27 | { 28 | } 29 | } -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Requests/Users/RefreshTokenRequestExample.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Requests.Users 2 | { 3 | using System; 4 | using Application.Users.Commands.Jwt.Refresh; 5 | using Swashbuckle.AspNetCore.Filters; 6 | 7 | public class RefreshTokenRequestExample : IExamplesProvider 8 | { 9 | private const string ExampleToken = 10 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiIxNDFlZjVlOS0wMmQ2LTQ2MTMtOGFmNS05NTE0NzM3YzI5YTEiLCJ1bmlxdWVfbmFtZSI6InRlc3RAdGVzdC5jb20iLCJuYmYiOjE1ODY3MDQ5ODEsImV4cCI6MTU4NzMwOTc4MSwiaWF0IjoxNTg2NzA0OTgxfQ.GTq2tA4KnCrBkcunnet5ijznq9Vy3NQJq1-znwz0vXI"; 11 | 12 | public JwtRefreshTokenCommand GetExamples() 13 | => new JwtRefreshTokenCommand { Token = ExampleToken, RefreshToken = Guid.NewGuid() }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Core/Application/Common/Models/Result.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Models 2 | { 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | public class Result 7 | { 8 | private Result(bool succeeded, string error, ErrorType errorType = ErrorType.General) 9 | { 10 | this.Succeeded = succeeded; 11 | this.Error = error; 12 | this.ErrorType = errorType; 13 | } 14 | 15 | public bool Succeeded { get; } 16 | 17 | public ErrorType ErrorType { get; } 18 | 19 | public string Error { get; } 20 | 21 | public static Result Success() 22 | => new Result(true, string.Empty); 23 | 24 | public static Result Failure(string error, ErrorType errorType = ErrorType.General) 25 | => new Result(false, error, errorType); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Configurations/BidConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Persistence.Configurations 2 | { 3 | using Domain.Entities; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | public class BidConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder 12 | .ToTable("Bids"); 13 | 14 | builder 15 | .HasKey(p => p.Id); 16 | 17 | builder 18 | .Property(p => p.Amount) 19 | .IsRequired(); 20 | 21 | builder 22 | .Property(p => p.UserId) 23 | .IsRequired(); 24 | 25 | builder 26 | .Property(p => p.ItemId); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Requests/Items/UpdateItemRequestExample.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Requests.Items 2 | { 3 | using System; 4 | using Application.Items.Commands.UpdateItem; 5 | using Swashbuckle.AspNetCore.Filters; 6 | 7 | public class UpdateItemRequestExample : IExamplesProvider 8 | { 9 | public UpdateItemCommand GetExamples() 10 | => new UpdateItemCommand 11 | { 12 | Id = Guid.NewGuid(), 13 | Title = "New title", 14 | Description = "New description", 15 | StartingPrice = 10000m, 16 | MinIncrease = 500m, 17 | StartTime = DateTime.UtcNow.AddDays(10), 18 | EndTime = DateTime.UtcNow.AddYears(1), 19 | SubCategoryId = Guid.NewGuid() 20 | }; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Migrations/20200515095428_RemoveBidTableMadeOnColumn.cs: -------------------------------------------------------------------------------- 1 | namespace Persistence.Migrations 2 | { 3 | using System; 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | 6 | public partial class RemoveBidTableMadeOnColumn : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.DropColumn( 11 | "MadeOn", 12 | "Bids"); 13 | } 14 | 15 | protected override void Down(MigrationBuilder migrationBuilder) 16 | { 17 | migrationBuilder.AddColumn( 18 | "MadeOn", 19 | "Bids", 20 | "datetime2", 21 | nullable: false, 22 | defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Items/List/Header/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Header.css"; 3 | import { Dropdown } from "react-bootstrap"; 4 | 5 | export const Header = ({ totalItemsCount }) => { 6 | return ( 7 |
8 |
9 |

All Items

10 |

Total Listing Found: {totalItemsCount}

11 |
12 |
13 | 14 | 19 | sort by 20 | 21 | 22 | 23 | TODO 24 | 25 | 26 |
27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /Tests/Application.UnitTests/Setup/IdentityMocker.cs: -------------------------------------------------------------------------------- 1 | namespace Application.UnitTests.Setup 2 | { 3 | using Domain.Entities; 4 | using Microsoft.AspNetCore.Identity; 5 | using Moq; 6 | 7 | public class IdentityMocker 8 | { 9 | public static Mock> GetMockedUserManager() 10 | { 11 | var userStoreMock = new Mock>(); 12 | return new Mock>( 13 | userStoreMock.Object, null, null, null, null, null, null, null, null); 14 | } 15 | 16 | public static Mock> GetMockedRoleManager() 17 | { 18 | var roleStoreMock = new Mock>(); 19 | var roleManager = new Mock>(roleStoreMock.Object, null, null, null, null); 20 | return roleManager; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Tests/Application.UnitTests/Setup/TestSetup.cs: -------------------------------------------------------------------------------- 1 | namespace Application.UnitTests.Setup 2 | { 3 | using AutoMapper; 4 | using global::Common.AutoMapping.Profiles; 5 | 6 | public static class TestSetup 7 | { 8 | private static IMapper mapper; 9 | private static readonly object Sync = new object(); 10 | private static bool mapperInitialized; 11 | 12 | public static IMapper InitializeMapper() 13 | { 14 | lock (Sync) 15 | { 16 | if (mapperInitialized) 17 | { 18 | return mapper; 19 | } 20 | 21 | var config = new MapperConfiguration(cfg => { cfg.AddProfile(); }); 22 | 23 | mapper = config.CreateMapper(); 24 | mapperInitialized = true; 25 | 26 | return mapper; 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Configurations/SubCategoryConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Persistence.Configurations 2 | { 3 | using Common; 4 | using Domain.Entities; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 7 | 8 | public class SubCategoryConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder 13 | .ToTable("SubCategories"); 14 | 15 | builder 16 | .HasKey(p => p.Id); 17 | 18 | builder 19 | .Property(p => p.Name) 20 | .IsRequired() 21 | .HasMaxLength(ModelConstants.SubCategory.NameMaxLength); 22 | 23 | builder 24 | .Property(p => p.CategoryId) 25 | .IsRequired(); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Responses/Users/LoginUserRequestResponseModel.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Responses.Users 2 | { 3 | using System; 4 | using Application.Common.Models; 5 | using Application.Users.Commands; 6 | using Swashbuckle.AspNetCore.Filters; 7 | 8 | public class LoginUserRequestResponseModel : IExamplesProvider> 9 | { 10 | private const string ExampleToken = 11 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiIxNDFlZjVlOS0wMmQ2LTQ2MTMtOGFmNS05NTE0NzM3YzI5YTEiLCJ1bmlxdWVfbmFtZSI6InRlc3RAdGVzdC5jb20iLCJuYmYiOjE1ODY3MDQ5ODEsImV4cCI6MTU4NzMwOTc4MSwiaWF0IjoxNTg2NzA0OTgxfQ.GTq2tA4KnCrBkcunnet5ijznq9Vy3NQJq1-znwz0vXI"; 12 | 13 | public Response GetExamples() 14 | => new Response(new AuthSuccessResponse { Token = ExampleToken, RefreshToken = Guid.NewGuid() }); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Presentation/Api/Services/CurrentUserService.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Services 2 | { 3 | using System.Security.Claims; 4 | using Application; 5 | using Application.Common.Interfaces; 6 | using Microsoft.AspNetCore.Http; 7 | 8 | public class CurrentUserService : ICurrentUserService 9 | { 10 | private readonly bool? hasAdminClaim; 11 | 12 | public CurrentUserService(IHttpContextAccessor httpContextAccessor) 13 | { 14 | this.UserId = httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier); 15 | this.IsAuthenticated = this.UserId != null; 16 | this.hasAdminClaim = httpContextAccessor.HttpContext?.User?.IsInRole(AppConstants.AdministratorRole); 17 | } 18 | 19 | public string UserId { get; } 20 | 21 | public bool IsAuthenticated { get; } 22 | 23 | public bool IsAdmin => this.hasAdminClaim ?? false; 24 | } 25 | } -------------------------------------------------------------------------------- /Tests/Api.IntegrationTests/Api.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Requests/Items/CreateItemRequestExample.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Requests.Items 2 | { 3 | using System; 4 | using Application.Items.Commands.CreateItem; 5 | using Swashbuckle.AspNetCore.Filters; 6 | 7 | public class CreateItemRequestExample : IExamplesProvider 8 | { 9 | private const int Ten = 10; 10 | 11 | public CreateItemCommand GetExamples() 12 | => new CreateItemCommand 13 | { 14 | Title = "Some really expensive item", 15 | Description = "This item was found in 1980.", 16 | StartingPrice = 10000, 17 | MinIncrease = 1000, 18 | StartTime = DateTime.UtcNow.AddMinutes(Ten), 19 | EndTime = DateTime.UtcNow.AddDays(Ten), 20 | SubCategoryId = Guid.Parse("5AB7CAEF-9B24-4B6D-A7A5-08D7DFB08A49") 21 | }; 22 | } 23 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/utils/hooks/useDebounce.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | const useDebounce = (value, delay) => { 4 | // State and setters for debounced value 5 | const [debouncedValue, setDebouncedValue] = useState(value); 6 | 7 | useEffect( 8 | () => { 9 | // Update debounced value after delay 10 | const handler = setTimeout(() => { 11 | setDebouncedValue(value); 12 | }, delay); 13 | 14 | // Cancel the timeout if value changes (also on delay change or unmount) 15 | // This is how we prevent debounced value from updating if value is changed ... 16 | // .. within the delay period. Timeout gets cleared and restarted. 17 | return () => { 18 | clearTimeout(handler); 19 | }; 20 | }, 21 | [value, delay] // Only re-call effect if value or delay changes 22 | ); 23 | 24 | return debouncedValue; 25 | }; 26 | 27 | export default useDebounce; 28 | -------------------------------------------------------------------------------- /src/Core/Application/Items/Commands/UpdateItem/UpdateItemCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Items.Commands.UpdateItem 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using MediatR; 6 | using Microsoft.AspNetCore.Http; 7 | 8 | public class UpdateItemCommand : IRequest 9 | { 10 | public Guid Id { get; set; } 11 | 12 | public string Title { get; set; } 13 | 14 | public string Description { get; set; } 15 | 16 | public decimal StartingPrice { get; set; } 17 | 18 | public decimal MinIncrease { get; set; } 19 | 20 | public DateTime StartTime { get; set; } 21 | 22 | public DateTime EndTime { get; set; } 23 | 24 | public Guid SubCategoryId { get; set; } 25 | 26 | public ICollection PicturesToAdd { get; set; } = new HashSet(); 27 | 28 | public ICollection PicturesToRemove { get; set; } = new HashSet(); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/Models/PaginationFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Models 2 | { 3 | using Admin.Queries.List; 4 | using global::Common.AutoMapping.Interfaces; 5 | using Items.Queries.List; 6 | 7 | public class PaginationFilter : IMapWith, IMapWith 8 | { 9 | private const int DefaultPageNumber = 1; 10 | 11 | public PaginationFilter() 12 | { 13 | this.PageNumber = DefaultPageNumber; 14 | this.PageSize = AppConstants.PageSize; 15 | } 16 | 17 | public PaginationFilter(int pageNumber, int pageSize) 18 | { 19 | this.PageNumber = pageNumber < DefaultPageNumber ? DefaultPageNumber : pageNumber; 20 | this.PageSize = pageSize >= AppConstants.PageSize || pageSize < 1 ? AppConstants.PageSize : pageSize; 21 | } 22 | 23 | public int PageNumber { get; set; } 24 | 25 | public int PageSize { get; set; } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Core/Application/Items/Queries/List/ListItemsResponseModel.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Items.Queries.List 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Domain.Entities; 6 | using global::Common.AutoMapping.Interfaces; 7 | using Pictures; 8 | 9 | public class ListItemsResponseModel : IMapWith 10 | { 11 | public Guid Id { get; set; } 12 | 13 | public string Title { get; set; } 14 | 15 | public string Description { get; set; } 16 | 17 | public decimal StartingPrice { get; set; } 18 | 19 | public decimal MinIncrease { get; set; } 20 | 21 | public DateTime StartTime { get; set; } 22 | 23 | public DateTime EndTime { get; set; } 24 | 25 | public string UserId { get; set; } 26 | 27 | public string UserFullName { get; set; } 28 | 29 | public Guid SubCategoryId { get; set; } 30 | 31 | public ICollection Pictures { get; set; } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Core/Application/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | namespace Application 2 | { 3 | using System.Reflection; 4 | using AutoMapper; 5 | using Common; 6 | using Common.Behaviours; 7 | using global::Common.AutoMapping.Profiles; 8 | using MediatR; 9 | using Microsoft.Extensions.DependencyInjection; 10 | 11 | public static class DependencyInjection 12 | { 13 | public static IServiceCollection AddApplication(this IServiceCollection services) 14 | { 15 | services 16 | .AddAutoMapper(typeof(DefaultProfile)); 17 | services 18 | .AddMediatR(Assembly.GetExecutingAssembly()); 19 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestValidationBehavior<,>)); 20 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestPerformanceBehaviour<,>)); 21 | services.AddHostedService(); 22 | return services; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Core/Domain/Entities/Item.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Entities 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Common; 6 | 7 | public class Item : AuditableEntity 8 | { 9 | public Guid Id { get; set; } 10 | public string Title { get; set; } 11 | public string Description { get; set; } 12 | public decimal StartingPrice { get; set; } 13 | public decimal MinIncrease { get; set; } 14 | public DateTime StartTime { get; set; } 15 | public DateTime EndTime { get; set; } 16 | public bool IsEmailSent { get; set; } = false; 17 | 18 | public string UserId { get; set; } 19 | public AuctionUser User { get; set; } 20 | 21 | public Guid SubCategoryId { get; set; } 22 | public SubCategory SubCategory { get; set; } 23 | 24 | public ICollection Bids { get; set; } = new HashSet(); 25 | public ICollection Pictures { get; set; } = new HashSet(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/Models/PagedResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Models 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | public class PagedResponse 7 | { 8 | public PagedResponse() 9 | { 10 | this.PageNumber = 1; 11 | this.PageSize = AppConstants.PageSize; 12 | } 13 | 14 | public PagedResponse(IEnumerable data, int totalDataCountInDatabase) 15 | { 16 | this.Data = data; 17 | this.TotalPages = (int) Math.Ceiling(totalDataCountInDatabase / (double) AppConstants.PageSize); 18 | } 19 | 20 | public int TotalPages { get; set; } 21 | 22 | public int PageNumber { get; set; } 23 | 24 | public int? PageSize { get; set; } 25 | 26 | public int? NextPage { get; set; } 27 | 28 | public int? PreviousPage { get; set; } 29 | 30 | public IEnumerable Data { get; set; } 31 | 32 | public int TotalDataCount { get; set; } 33 | } 34 | } -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Configurations/CategoryConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Persistence.Configurations 2 | { 3 | using Common; 4 | using Domain.Entities; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 7 | 8 | public class CategoryConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder 13 | .ToTable("Categories"); 14 | 15 | builder 16 | .HasKey(p => p.Id); 17 | 18 | builder 19 | .Property(p => p.Name) 20 | .IsRequired() 21 | .HasMaxLength(ModelConstants.Category.NameMaxLength); 22 | 23 | builder 24 | .HasMany(x => x.SubCategories) 25 | .WithOne(c => c.Category) 26 | .HasForeignKey(c => c.CategoryId) 27 | .OnDelete(DeleteBehavior.Restrict); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Bid/BidHigherButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button, Spinner } from "react-bootstrap"; 3 | 4 | export const BidHigherButton = ({ 5 | isLoading, 6 | handleOnClick, 7 | amount, 8 | percentage, 9 | }) => { 10 | const bid = percentage <= 0 ? amount : amount + (amount * percentage) / 100; 11 | const suggestedBid = parseFloat(bid).toFixed(2); 12 | 13 | return !isLoading ? ( 14 | 25 | ) : ( 26 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/Core/Application/Users/Commands/ConfirmEmail/ConfirmEmailCommandHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Users.Commands.ConfirmEmail 2 | { 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Common.Exceptions; 6 | using Common.Interfaces; 7 | using MediatR; 8 | 9 | public class ConfirmEmailCommandHandler : IRequestHandler 10 | { 11 | private readonly IUserManager userManager; 12 | 13 | public ConfirmEmailCommandHandler(IUserManager userManager) 14 | { 15 | this.userManager = userManager; 16 | } 17 | 18 | public async Task Handle(ConfirmEmailCommand request, CancellationToken cancellationToken) 19 | { 20 | var result = await this.userManager.ConfirmEmail(request.Email, request.Code); 21 | if (!result) 22 | { 23 | throw new BadRequestException(ExceptionMessages.User.EmailVerificationFailed); 24 | } 25 | 26 | return Unit.Value; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Responses/Pictures/SuccessfulPictureUploadResponseModel.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Responses.Pictures 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Application.Common.Models; 6 | using Application.Pictures; 7 | using Swashbuckle.AspNetCore.Filters; 8 | 9 | public class SuccessfulPictureUploadResponseModel : IExamplesProvider> 10 | { 11 | public MultiResponse GetExamples() 12 | => new MultiResponse(new HashSet 13 | { 14 | new PictureResponseModel 15 | { 16 | Id = Guid.NewGuid(), 17 | Url = "image url (i.e https://google.com)" 18 | }, 19 | new PictureResponseModel 20 | { 21 | Id = Guid.NewGuid(), 22 | Url = "Some random url" 23 | } 24 | }); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Core/Application/Items/Queries/Details/ItemDetailsResponseModel.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Items.Queries.Details 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Domain.Entities; 6 | using global::Common.AutoMapping.Interfaces; 7 | using Pictures; 8 | 9 | public class ItemDetailsResponseModel : IMapWith 10 | { 11 | public Guid Id { get; set; } 12 | 13 | public string Title { get; set; } 14 | 15 | public string Description { get; set; } 16 | 17 | public decimal StartingPrice { get; set; } 18 | 19 | public decimal MinIncrease { get; set; } 20 | 21 | public DateTime StartTime { get; set; } 22 | 23 | public DateTime EndTime { get; set; } 24 | 25 | public string UserId { get; set; } 26 | 27 | public string UserFullName { get; set; } 28 | 29 | public ICollection Pictures { get; set; } 30 | 31 | public Guid SubCategoryId { get; set; } 32 | 33 | public string SubCategoryName { get; set; } 34 | } 35 | } -------------------------------------------------------------------------------- /src/Core/Application/Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Core/Application/Users/Commands/Jwt/GenerateJwtTokenCommandHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Users.Commands.Jwt 2 | { 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using AppSettingsModels; 6 | using Common.Interfaces; 7 | using global::Common; 8 | using MediatR; 9 | using Microsoft.Extensions.Options; 10 | 11 | public class GenerateJwtTokenCommandHandler : BaseJwtTokenHandler, 12 | IRequestHandler 13 | { 14 | public GenerateJwtTokenCommandHandler( 15 | IOptions options, 16 | IUserManager userManager, 17 | IAuctionSystemDbContext context, 18 | IDateTime dateTime) 19 | : base(options, userManager, context, dateTime) 20 | { 21 | } 22 | 23 | public async Task Handle(GenerateJwtTokenCommand request, 24 | CancellationToken cancellationToken) 25 | => await this.GenerateAuthResponse(request.UserId, request.Username, cancellationToken); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Home/LiveItems/Pictures/PictureContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import "../Pictures/PictureContainer.css"; 5 | 6 | export const PictureContainer = ({ pictures, itemSlug }) => { 7 | const pictureClassNameRetriever = (index) => { 8 | if (index === 1) { 9 | return "primary"; 10 | } 11 | 12 | return "secondary"; 13 | }; 14 | 15 | return ( 16 |
17 | {pictures.slice(0, 3).map((picture, index) => { 18 | return ( 19 | 24 | primary 31 | 32 | ); 33 | })} 34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/Core/Application/Common/Behaviours/RequestLogger.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Behaviours 2 | { 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Interfaces; 6 | using MediatR.Pipeline; 7 | using Microsoft.Extensions.Logging; 8 | 9 | public class RequestLogger : IRequestPreProcessor 10 | { 11 | private readonly ICurrentUserService currentUserService; 12 | private readonly ILogger logger; 13 | 14 | public RequestLogger(ILogger logger, ICurrentUserService currentUserService) 15 | { 16 | this.logger = logger; 17 | this.currentUserService = currentUserService; 18 | } 19 | 20 | public Task Process(TRequest request, CancellationToken cancellationToken) 21 | { 22 | var name = typeof(TRequest).Name; 23 | 24 | this.logger.LogInformation("AuctionSystem Request: {Name} {@UserId} {@Request}", 25 | name, this.currentUserService.UserId, request); 26 | 27 | return Task.CompletedTask; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Configurations/AuctionUserConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Persistence.Configurations 2 | { 3 | using Common; 4 | using Domain.Entities; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 7 | 8 | public class AuctionUserConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder 13 | .Property(p => p.FullName) 14 | .IsRequired() 15 | .HasMaxLength(ModelConstants.User.FullNameMaxLength); 16 | 17 | builder 18 | .HasMany(b => b.Bids) 19 | .WithOne(u => u.User) 20 | .HasForeignKey(u => u.UserId) 21 | .OnDelete(DeleteBehavior.Restrict); 22 | 23 | builder 24 | .HasMany(b => b.ItemsSold) 25 | .WithOne(u => u.User) 26 | .HasForeignKey(u => u.UserId) 27 | .OnDelete(DeleteBehavior.Restrict); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Melik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Error/NetworkError.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { faServer } from "@fortawesome/free-solid-svg-icons"; 4 | import { history } from "../.."; 5 | import "./Error.css"; 6 | 7 | export const NetworkError = () => { 8 | const [secondsCounter, setSecondsCounter] = useState(60); 9 | 10 | useEffect(() => { 11 | const interval = 12 | secondsCounter >= 0 && 13 | setInterval(() => setSecondsCounter(secondsCounter - 1), 1000); 14 | 15 | if (secondsCounter === 0) { 16 | history.push(history.location.state); 17 | } 18 | return () => clearInterval(interval); 19 | }, [secondsCounter]); 20 | return ( 21 |
22 | 23 |
24 | We're sorry for the inconvenience but unfortunately our server is down. 25 | Please try again later! 26 |
27 |

28 | Auto retry in {secondsCounter} seconds... 29 |

30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /Tests/Application.UnitTests/Setup/DataConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Application.UnitTests.Setup 2 | { 3 | using System; 4 | 5 | public static class DataConstants 6 | { 7 | public const string SampleItemTitle = "SampleTitle"; 8 | public const string SampleItemDescription = "Very cool item"; 9 | public const decimal SampleItemStartingPrice = 300; 10 | public const decimal SampleItemMinIncrease = 10; 11 | public static readonly string SampleUserId = "8490931b-67b9-4784-a231-99c898636ee6"; 12 | public static readonly string SampleAdminUserId = "0ef2317e-326e-4457-8cf9-7ee57ea4385f"; 13 | 14 | public static readonly Guid SampleItemId = Guid.Parse("342b77c3-89de-4b37-9969-baa91c1573f0"); 15 | public static readonly DateTime SampleItemEndTime = DateTime.MaxValue; 16 | 17 | public static readonly Guid SampleSubCategoryId = Guid.Parse("386c2db8-5b8a-4744-8f60-7fcd3b1c7653"); 18 | public static readonly Guid SampleCategoryId = Guid.Parse("fe712d64-226e-4007-8bf2-76c0ebb12e96"); 19 | 20 | public static readonly Guid SamplePictureId = Guid.Parse("73716e91-4989-488e-8f77-dcf46b126c3b"); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { Route } from "react-router-dom"; 3 | import { useAuth } from "../utils/hooks/authHook"; 4 | import { toast } from "react-toastify"; 5 | import { history } from ".."; 6 | 7 | export const PrivateRoute = ({ 8 | component: Component, 9 | adminOnly = false, 10 | ...rest 11 | }) => { 12 | const { user } = useAuth(); 13 | 14 | return ( 15 | { 18 | if (user) { 19 | if (adminOnly && user.isAdmin) { 20 | return ; 21 | } 22 | if (adminOnly && !user.isAdmin) { 23 | history.push("/notFound"); 24 | return; 25 | } 26 | if (!adminOnly && user) { 27 | return ; 28 | } 29 | } else { 30 | return ( 31 | 32 | {history.push("/sign-in", history.location.pathname)} 33 | {toast.warning("Please sign in!")} 34 | 35 | ); 36 | } 37 | }} 38 | /> 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/Presentation/Api/Extensions/ConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Extensions 2 | { 3 | using Microsoft.Extensions.Configuration; 4 | 5 | public static class ConfigurationExtensions 6 | { 7 | public static IConfigurationSection GetJwtSecretSection(this IConfiguration configuration) 8 | => configuration.GetSection("JwtSettings"); 9 | 10 | public static IConfigurationSection GetRedisSection(this IConfiguration configuration) 11 | => configuration.GetSection("RedisCacheSettings"); 12 | 13 | public static string GetSendGridApiKey(this IConfiguration configuration) 14 | => configuration.GetSection("SendGrid:ApiKey").Value; 15 | 16 | public static string GetCloudinaryCloudName(this IConfiguration configuration) 17 | => configuration.GetSection("Cloudinary:CloudName").Value; 18 | 19 | public static string GetCloudinaryApiKey(this IConfiguration configuration) 20 | => configuration.GetSection("Cloudinary:ApiKey").Value; 21 | 22 | public static string GetCloudinaryApiSecret(this IConfiguration configuration) 23 | => configuration.GetSection("Cloudinary:ApiSecret").Value; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Infrastructure/AuctionSystem.Infrastructure/EmailSender.cs: -------------------------------------------------------------------------------- 1 | namespace AuctionSystem.Infrastructure 2 | { 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | using Application.AppSettingsModels; 6 | using Application.Common.Interfaces; 7 | using Microsoft.Extensions.Options; 8 | using SendGrid; 9 | using SendGrid.Helpers.Mail; 10 | 11 | public class EmailSender : IEmailSender 12 | { 13 | private readonly SendGridOptions options; 14 | 15 | public EmailSender(IOptions options) 16 | { 17 | this.options = options.Value; 18 | } 19 | 20 | public async Task SendEmailAsync(string sender, string receiver, string subject, string htmlMessage) 21 | { 22 | var client = new SendGridClient(this.options.SendGridApiKey); 23 | var from = new EmailAddress(sender); 24 | var to = new EmailAddress(receiver, receiver); 25 | var msg = MailHelper.CreateSingleEmail(from, to, subject, htmlMessage, htmlMessage); 26 | var isSuccessful = await client.SendEmailAsync(msg); 27 | 28 | return isSuccessful.StatusCode == HttpStatusCode.Accepted; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/Presentation/Api/Controllers/CategoriesController.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Controllers 2 | { 3 | using System.Threading.Tasks; 4 | using Api.Common; 5 | using Application.Categories.Queries.List; 6 | using Application.Common.Models; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Mvc; 9 | using SwaggerExamples; 10 | using Swashbuckle.AspNetCore.Annotations; 11 | 12 | public class CategoriesController : BaseController 13 | { 14 | private const int CachedTimeInMinutes = 3600; 15 | 16 | [HttpGet] 17 | [Cached(CachedTimeInMinutes)] 18 | [SwaggerResponse( 19 | StatusCodes.Status200OK, 20 | SwaggerDocumentation.CategoriesConstants.SuccessfulGetRequestMessage, 21 | typeof(MultiResponse))] 22 | [SwaggerResponse( 23 | StatusCodes.Status404NotFound, 24 | SwaggerDocumentation.CategoriesConstants.BadRequestDescriptionMessage, 25 | typeof(NotFoundErrorModel))] 26 | public async Task Get() 27 | { 28 | var result = await this.Mediator.Send(new ListCategoriesQuery()); 29 | return this.Ok(result); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/Exceptions/ValidationException.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Exceptions 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using FluentValidation.Results; 7 | 8 | public class ValidationException : Exception 9 | { 10 | public ValidationException() 11 | : base("One or more validation failures have occurred.") 12 | { 13 | this.Failures = new Dictionary(); 14 | } 15 | 16 | public ValidationException(IReadOnlyCollection failures) 17 | : this() 18 | { 19 | var propertyNames = failures 20 | .Select(e => e.PropertyName) 21 | .Distinct(); 22 | 23 | foreach (var propertyName in propertyNames) 24 | { 25 | var propertyFailures = failures 26 | .Where(e => e.PropertyName == propertyName) 27 | .Select(e => e.ErrorMessage) 28 | .ToArray(); 29 | 30 | this.Failures.Add(propertyName, propertyFailures); 31 | } 32 | } 33 | 34 | public IDictionary Failures { get; } 35 | } 36 | } -------------------------------------------------------------------------------- /src/Core/Application/SeedSampleData/SeedSampleDataCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Application.SeedSampleData 2 | { 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Common.Interfaces; 6 | using global::Common; 7 | using MediatR; 8 | 9 | public class SeedSampleDataCommand : IRequest { } 10 | 11 | public class SeedSampleDataCommandHandler : IRequestHandler 12 | { 13 | private readonly IAuctionSystemDbContext context; 14 | private readonly IDateTime dateTime; 15 | private readonly IUserManager userManager; 16 | 17 | public SeedSampleDataCommandHandler(IAuctionSystemDbContext context, 18 | IDateTime dateTime, 19 | IUserManager userManager) 20 | { 21 | this.context = context; 22 | this.dateTime = dateTime; 23 | this.userManager = userManager; 24 | } 25 | 26 | 27 | public async Task Handle(SeedSampleDataCommand request, CancellationToken cancellationToken) 28 | { 29 | var seeder = new Seeder(this.context, this.dateTime, this.userManager); 30 | await seeder.SeedAllAsync(cancellationToken); 31 | return Unit.Value; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Persistence.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | aefa38f9-375a-46f3-8107-2570034c62e7 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Tests/Application.UnitTests/Categories/Queries/ListCategoriesQueryHandlerTests.cs: -------------------------------------------------------------------------------- 1 | namespace Application.UnitTests.Categories.Queries 2 | { 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Application.Categories.Queries.List; 7 | using AutoMapper; 8 | using Common.Interfaces; 9 | using FluentAssertions; 10 | using Setup; 11 | using Xunit; 12 | 13 | [Collection("QueryCollection")] 14 | public class ListCategoriesQueryHandlerTests 15 | { 16 | private readonly IAuctionSystemDbContext context; 17 | private readonly IMapper mapper; 18 | 19 | public ListCategoriesQueryHandlerTests(QueryTestFixture fixture) 20 | { 21 | this.context = fixture.Context; 22 | this.mapper = fixture.Mapper; 23 | } 24 | 25 | [Fact] 26 | public async Task GetCategories_Should_Return_Correct_Count() 27 | { 28 | var handler = new ListCategoriesQueryHandler(this.context, this.mapper); 29 | var result = await handler.Handle(new ListCategoriesQuery(), CancellationToken.None); 30 | 31 | result 32 | .Data 33 | .Should() 34 | .HaveCount(this.context.Categories.Count()); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /Tests/Persistence.IntegrationTests/Persistence.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Responses/Items/ItemDetails.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Responses.Items 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Application.Common.Models; 6 | using Application.Items.Queries.Details; 7 | using Application.Pictures; 8 | using Swashbuckle.AspNetCore.Filters; 9 | 10 | public class ItemDetails : IExamplesProvider> 11 | { 12 | public Response GetExamples() 13 | => new Response(new ItemDetailsResponseModel 14 | { 15 | Id = Guid.Parse("46B33009-243D-4765-872E-08D7DFB08A87"), 16 | Title = "Test Title_1", 17 | Description = "Test Description_1", 18 | StartingPrice = 10000.00m, 19 | MinIncrease = 5.00m, 20 | StartTime = DateTime.UtcNow, 21 | EndTime = DateTime.UtcNow.AddDays(10), 22 | UserFullName = "Melik Pehlivanov", 23 | SubCategoryName = "Antiques", 24 | Pictures = new List 25 | { 26 | new PictureResponseModel { Id = Guid.NewGuid(), Url = "Some example url here" } 27 | } 28 | }); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Presentation/Api/MiddleWares/AuthorizationHeaderMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Middlewares 2 | { 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Http; 6 | 7 | public class AuthorizationHeaderMiddleware 8 | { 9 | private readonly RequestDelegate next; 10 | private const string AuthorizationHeader = "Authorization"; 11 | 12 | public AuthorizationHeaderMiddleware(RequestDelegate next) 13 | { 14 | this.next = next; 15 | } 16 | 17 | public async Task Invoke(HttpContext context) 18 | { 19 | context.Request.Cookies.TryGetValue(ApiConstants.RefreshToken, out var refreshToken); 20 | context.Request.Cookies.TryGetValue(ApiConstants.JwtToken, out var jwtToken); 21 | 22 | if (jwtToken != null && refreshToken != null) 23 | { 24 | context.Request.Headers.Append(AuthorizationHeader, $"Bearer {jwtToken}"); 25 | } 26 | 27 | await this.next(context); 28 | } 29 | } 30 | 31 | public static class AuthorizationHeaderMiddlewareExtensions 32 | { 33 | public static IApplicationBuilder UseAuthorizationHeader(this IApplicationBuilder builder) 34 | => builder.UseMiddleware(); 35 | } 36 | } -------------------------------------------------------------------------------- /src/Presentation/Api/Services/ResponseCacheService.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Services 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using Application.Common.Interfaces; 6 | using Microsoft.Extensions.Caching.Distributed; 7 | using Newtonsoft.Json; 8 | 9 | public class ResponseCacheService : IResponseCacheService 10 | { 11 | private readonly IDistributedCache distributedCache; 12 | 13 | public ResponseCacheService(IDistributedCache distributedCache) 14 | { 15 | this.distributedCache = distributedCache; 16 | } 17 | 18 | public async Task CacheResponseAsync(string key, object response, TimeSpan cachingTime) 19 | { 20 | if (response == null) 21 | { 22 | return; 23 | } 24 | 25 | await this.distributedCache 26 | .SetStringAsync( 27 | key, 28 | JsonConvert.SerializeObject(response), 29 | new DistributedCacheEntryOptions 30 | { 31 | AbsoluteExpirationRelativeToNow = cachingTime 32 | }); 33 | } 34 | 35 | public async Task GetCachedResponseAsync(string cacheKey) 36 | => await this.distributedCache.GetStringAsync(cacheKey); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/Application.UnitTests/Application.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Core/Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Users.Commands.CreateUser 2 | { 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Common; 6 | using Common.Exceptions; 7 | using Common.Interfaces; 8 | using MediatR; 9 | 10 | public class CreateUserCommandHandler : IRequestHandler 11 | { 12 | private readonly IUserManager userManager; 13 | private readonly IEmailSender emailSender; 14 | 15 | public CreateUserCommandHandler(IUserManager userManager, IEmailSender emailSender) 16 | { 17 | this.userManager = userManager; 18 | this.emailSender = emailSender; 19 | } 20 | 21 | public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken) 22 | { 23 | var result = await this.userManager.CreateUserAsync(request.Email, request.Password, request.FullName); 24 | if (!result.Succeeded) 25 | { 26 | throw new BadRequestException(ExceptionMessages.User.UserNotCreatedSuccessfully); 27 | } 28 | 29 | var token = await this.userManager.GenerateEmailConfirmationCode(request.Email); 30 | await this.emailSender.SendConfirmationEmail(request.Email, token); 31 | 32 | return Unit.Value; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/Core/Application/Categories/Queries/List/ListCategoriesQueryHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Categories.Queries.List 2 | { 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using AutoMapper; 6 | using AutoMapper.QueryableExtensions; 7 | using Common.Interfaces; 8 | using Common.Models; 9 | using MediatR; 10 | using Microsoft.EntityFrameworkCore; 11 | 12 | public class 13 | ListCategoriesQueryHandler : IRequestHandler> 14 | { 15 | private readonly IAuctionSystemDbContext context; 16 | private readonly IMapper mapper; 17 | 18 | public ListCategoriesQueryHandler(IAuctionSystemDbContext context, IMapper mapper) 19 | { 20 | this.context = context; 21 | this.mapper = mapper; 22 | } 23 | 24 | public async Task> Handle(ListCategoriesQuery request, 25 | CancellationToken cancellationToken) 26 | { 27 | var categories = await this.context 28 | .Categories 29 | .Include(c => c.SubCategories) 30 | .ProjectTo(this.mapper.ConfigurationProvider) 31 | .ToListAsync(cancellationToken); 32 | 33 | return new MultiResponse(categories); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Bid/Chat/Chat.css: -------------------------------------------------------------------------------- 1 | .chat { 2 | background: #f2f5f8; 3 | color: #434651; 4 | overflow-y: auto; 5 | margin: 0; 6 | padding: 0 0 50px 0; 7 | margin-top: 60px; 8 | margin-bottom: 10px; 9 | height: 85vh; 10 | } 11 | 12 | .chat .chat-history { 13 | padding: 30px 20px 0 0; 14 | height: 575px; 15 | } 16 | 17 | .message { 18 | padding: 0.5rem; 19 | overflow: hidden; 20 | display: flex; 21 | color: #fff; 22 | padding: 1.1rem; 23 | border-radius: 7px; 24 | margin-top: 30px; 25 | } 26 | 27 | .message.my-message { 28 | background: #00008b; 29 | } 30 | .message.other-message { 31 | background: #94c2ed; 32 | } 33 | 34 | .message.highest-bid-message { 35 | background: green; 36 | } 37 | 38 | ul { 39 | list-style-type: none; 40 | } 41 | 42 | .pre-scrollable { 43 | max-height: 70vh; 44 | overflow-y: scroll; 45 | } 46 | 47 | .highest-bid-message:first-child { 48 | -webkit-animation-name: fadeInRight; 49 | animation-name: fadeInRight; 50 | animation-duration: 2s; 51 | } 52 | 53 | @-webkit-keyframes fadeInRight { 54 | 0% { 55 | opacity: 0; 56 | -webkit-transform: translateX(20px); 57 | } 58 | 100% { 59 | opacity: 1; 60 | -webkit-transform: translateX(0); 61 | } 62 | } 63 | 64 | @keyframes fadeInRight { 65 | 0% { 66 | opacity: 0; 67 | transform: translateX(20px); 68 | } 69 | 100% { 70 | opacity: 1; 71 | transform: translateX(0); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Core/Application/Common/Helpers/PaginationHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Helpers 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using Models; 7 | 8 | public static class PaginationHelper 9 | { 10 | public static PagedResponse CreatePaginatedResponse( 11 | PaginationFilter pagination, 12 | List response, 13 | int totalDataCountInDatabase) 14 | { 15 | var totalPages = (int) Math.Ceiling(totalDataCountInDatabase / (double) pagination.PageSize); 16 | var nextPage = pagination.PageNumber >= 1 && pagination.PageNumber < totalPages 17 | ? pagination.PageNumber + 1 18 | : (int?) null; 19 | var previousPage = pagination.PageNumber - 1 >= 1 20 | ? pagination.PageNumber - 1 21 | : (int?) null; 22 | 23 | return new PagedResponse 24 | { 25 | Data = response, 26 | PageNumber = pagination.PageNumber >= 1 ? pagination.PageNumber : 1, 27 | PageSize = pagination.PageSize >= 1 ? pagination.PageSize : (int?) null, 28 | NextPage = response.Any() ? nextPage : null, 29 | PreviousPage = previousPage, 30 | TotalPages = totalPages, 31 | TotalDataCount = totalDataCountInDatabase 32 | }; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/Interfaces/IUserManager.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Interfaces 2 | { 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Domain.Entities; 7 | using Microsoft.AspNetCore.Identity; 8 | using Models; 9 | 10 | public interface IUserManager 11 | { 12 | Task GetUserByIdAsync(string id); 13 | 14 | Task CreateUserAsync(string email, string password, string fullName); 15 | 16 | Task CreateUserAsync(AuctionUser user, string password); 17 | 18 | Task<(Result Result, string UserId)> SignIn(string email, string password); 19 | 20 | Task CreateRoleAsync(IdentityRole role); 21 | 22 | Task AddToRoleAsync(AuctionUser user, string role); 23 | 24 | Task AddToRoleAsync(string email, string role, string currentUserId); 25 | 26 | Task> GetUserRolesAsync(string userId); 27 | 28 | Task GetFirstUserId(); 29 | 30 | Task> GetUsersInRoleAsync(string role); 31 | 32 | Task RemoveFromRoleAsync( 33 | string username, 34 | string role, 35 | string currentUserId, 36 | CancellationToken cancellationToken); 37 | 38 | Task GenerateEmailConfirmationCode(string email); 39 | 40 | Task ConfirmEmail(string email, string code); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/Behaviours/RequestValidationBehaviour.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Behaviours 2 | { 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using FluentValidation; 8 | using MediatR; 9 | using ValidationException = Exceptions.ValidationException; 10 | 11 | public class RequestValidationBehavior : IPipelineBehavior 12 | where TRequest : IRequest 13 | { 14 | private readonly IEnumerable> validators; 15 | 16 | public RequestValidationBehavior(IEnumerable> validators) 17 | { 18 | this.validators = validators; 19 | } 20 | 21 | public Task Handle(TRequest request, 22 | CancellationToken cancellationToken, 23 | RequestHandlerDelegate next) 24 | { 25 | var context = new ValidationContext(request); 26 | 27 | var failures = this.validators 28 | .Select(v => v.Validate(context)) 29 | .SelectMany(result => result.Errors) 30 | .Where(f => f != null) 31 | .ToList(); 32 | 33 | if (failures.Count != 0) 34 | { 35 | throw new ValidationException(failures); 36 | } 37 | 38 | return next(); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Presentation/Api/Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | true 9 | $(NoWarn);1591 10 | 11 | 12 | 13 | true 14 | $(NoWarn);1591 15 | aefa38f9-375a-46f3-8107-2570034c62e7 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Core/Application/Users/Commands/Logout/LogoutUserCommandHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Users.Commands.Logout 2 | { 3 | using System; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Common.Interfaces; 8 | using MediatR; 9 | using Microsoft.EntityFrameworkCore; 10 | 11 | public class LogoutUserCommandHandler : IRequestHandler 12 | { 13 | private readonly IAuctionSystemDbContext context; 14 | 15 | public LogoutUserCommandHandler(IAuctionSystemDbContext context) 16 | { 17 | this.context = context; 18 | } 19 | 20 | public async Task Handle(LogoutUserCommand request, CancellationToken cancellationToken) 21 | { 22 | // If we don't have refresh token it means that user is already logged out 23 | if (request.RefreshToken == null) 24 | { 25 | return Unit.Value; 26 | } 27 | 28 | var refreshToken = await this.context 29 | .RefreshTokens 30 | .Where(r => r.Token == Guid.Parse(request.RefreshToken)) 31 | .SingleOrDefaultAsync(cancellationToken); 32 | refreshToken.Invalidated = true; 33 | 34 | this.context.RefreshTokens.Update(refreshToken); 35 | await this.context.SaveChangesAsync(cancellationToken); 36 | 37 | return Unit.Value; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/utils/hooks/useItemsSearch.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useCallback } from "react"; 2 | import itemsService from "../../services/itemsService"; 3 | 4 | const useItemsSearch = (query, pageNumber, setPageNumber) => { 5 | const [loading, setLoading] = useState(true); 6 | const [error, setError] = useState(false); 7 | const [items, setItems] = useState([]); 8 | const [totalItemsCount, setTotalItemsCount] = useState(0); 9 | const [hasMore, setHasMore] = useState(false); 10 | 11 | useEffect(() => { 12 | setItems([]); 13 | setPageNumber(1); 14 | }, [query, setPageNumber]); 15 | 16 | const makeRequest = useCallback(() => { 17 | setLoading(true); 18 | setError(false); 19 | 20 | query.pageNumber = pageNumber; 21 | query.pageSize = 10; 22 | 23 | itemsService 24 | .getItems(query) 25 | .then((result) => { 26 | setItems((previtems) => { 27 | return [...previtems, ...result.data.data]; 28 | }); 29 | setTotalItemsCount(result.data.totalDataCount); 30 | setHasMore( 31 | result.data.totalDataCount > result.data.pageSize && 32 | result.data.data.length > 0 33 | ); 34 | setLoading(false); 35 | }) 36 | .catch(() => setError(true)); 37 | }, [query, pageNumber]); 38 | 39 | return { 40 | makeRequest, 41 | loading, 42 | error, 43 | items, 44 | totalItemsCount, 45 | hasMore, 46 | }; 47 | }; 48 | 49 | export default useItemsSearch; 50 | -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Responses/Admin/SuccessfulAdminGetRequestResponseModel.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Responses.Admin 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Application.Admin.Queries.List; 6 | using Application.Common.Models; 7 | using Swashbuckle.AspNetCore.Filters; 8 | 9 | public class SuccessfulAdminGetRequestResponseModel : IExamplesProvider> 10 | { 11 | public PagedResponse GetExamples() 12 | => new PagedResponse(new List 13 | { 14 | new ListAllUsersResponseModel 15 | { 16 | Id = Guid.NewGuid().ToString(), 17 | Email = "admin@admin.com", 18 | FullName = "Admin Admin", 19 | CurrentRoles = new List 20 | { "User, Administrator" }, 21 | NonCurrentRoles = new List() 22 | }, 23 | new ListAllUsersResponseModel 24 | { 25 | Id = Guid.NewGuid().ToString(), 26 | Email = "normal@normal.com", 27 | FullName = "Normal User", 28 | CurrentRoles = new List { "User" }, 29 | NonCurrentRoles = new List { "Administrator" } 30 | } 31 | }, 2); 32 | } 33 | } -------------------------------------------------------------------------------- /src/Core/Application/Items/Commands/CreateItem/CreateItemCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Items.Commands.CreateItem 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using AutoMapper; 6 | using Common.Models; 7 | using Domain.Entities; 8 | using global::Common.AutoMapping.Interfaces; 9 | using MediatR; 10 | using Microsoft.AspNetCore.Http; 11 | 12 | public class CreateItemCommand : IRequest>, IMapWith, IHaveCustomMapping 13 | { 14 | public string Title { get; set; } 15 | 16 | public string Description { get; set; } 17 | 18 | public decimal StartingPrice { get; set; } 19 | 20 | public decimal MinIncrease { get; set; } 21 | 22 | public DateTime StartTime { get; set; } 23 | 24 | public DateTime EndTime { get; set; } 25 | 26 | public Guid SubCategoryId { get; set; } 27 | 28 | public ICollection Pictures { get; set; } = new HashSet(); 29 | 30 | public void ConfigureMapping(Profile mapper) 31 | { 32 | mapper 33 | .CreateMap() 34 | .ForMember(dest => dest.StartTime, 35 | opt => opt.MapFrom(src => src.StartTime.ToUniversalTime())) 36 | .ForMember(dest => dest.EndTime, 37 | opt => opt.MapFrom(src => src.EndTime.ToUniversalTime())) 38 | .ForMember(p => p.Pictures, opt => opt.Ignore()); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Presentation/Api/Models/ItemsFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | using Application.Items.Queries.List; 6 | using global::Common.AutoMapping.Interfaces; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | public class ItemsFilter : IMapWith 10 | { 11 | private const string DecimalMaxValue = "79228162514264337593543950335"; 12 | 13 | [FromQuery(Name = "title")] 14 | public string Title { get; set; } 15 | 16 | [FromQuery(Name = "userId")] 17 | public string UserId { get; set; } 18 | 19 | [FromQuery(Name = "minPrice")] 20 | [Range(typeof(decimal), "0.01", DecimalMaxValue)] 21 | public decimal? MinPrice { get; set; } 22 | 23 | [FromQuery(Name = "maxPrice")] 24 | [Range(typeof(decimal), "0.01", DecimalMaxValue)] 25 | public decimal? MaxPrice { get; set; } 26 | 27 | [FromQuery(Name = "getLiveItems")] 28 | public bool? GetLiveItems { get; set; } 29 | 30 | [FromQuery(Name = "startTime")] 31 | public DateTime? StartTime { get; set; } 32 | 33 | [FromQuery(Name = "endTime")] 34 | public DateTime? EndTime { get; set; } 35 | 36 | [FromQuery(Name = "minimumPicturesCount")] 37 | [Range(1, int.MaxValue)] 38 | public int? MinimumPicturesCount { get; set; } 39 | 40 | [FromQuery(Name = "subCategoryId")] 41 | public Guid SubCategoryId { get; set; } 42 | } 43 | } -------------------------------------------------------------------------------- /src/Core/Application/Bids/Queries/Details/GetHighestBidDetailsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Bids.Queries.Details 2 | { 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using AutoMapper; 7 | using AutoMapper.QueryableExtensions; 8 | using Common.Interfaces; 9 | using Common.Models; 10 | using MediatR; 11 | using Microsoft.EntityFrameworkCore; 12 | 13 | public class GetHighestBidDetailsQueryHandler : IRequestHandler> 15 | { 16 | private readonly IAuctionSystemDbContext context; 17 | private readonly IMapper mapper; 18 | 19 | public GetHighestBidDetailsQueryHandler(IAuctionSystemDbContext context, IMapper mapper) 20 | { 21 | this.context = context; 22 | this.mapper = mapper; 23 | } 24 | 25 | public async Task> Handle(GetHighestBidDetailsQuery request, 26 | CancellationToken cancellationToken) 27 | { 28 | var bid = await this.context 29 | .Bids 30 | .Where(b => b.ItemId == request.ItemId) 31 | .OrderByDescending(b => b.Amount) 32 | .ProjectTo(this.mapper.ConfigurationProvider) 33 | .FirstOrDefaultAsync(cancellationToken); 34 | 35 | return new Response(bid); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Bid/Chat/Chat.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useAuth } from "../../../utils/hooks/authHook"; 3 | import "./Chat.css"; 4 | 5 | export const Chat = ({ messages }) => { 6 | const auth = useAuth(); 7 | 8 | return ( 9 |
10 |

System messages

11 | Highest bid is highlighted with green background 12 |
13 |
    14 | {messages.map((_, index) => { 15 | let message = messages[messages.length - 1 - index]; 16 | return message.userId === auth.user.id ? ( 17 |
  • 23 | You've successfully bid {process.env.REACT_APP_CURRENCY_SIGN} 24 | {message.bidAmount} 25 |
  • 26 | ) : ( 27 |
  • 33 | {process.env.REACT_APP_CURRENCY_SIGN} 34 | {message.bidAmount}: Competing Bid 35 |
  • 36 | ); 37 | })} 38 |
39 |
40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/Core/Application/Items/Queries/Details/GetItemDetailsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Items.Queries.Details 2 | { 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using AutoMapper; 6 | using AutoMapper.QueryableExtensions; 7 | using Common.Exceptions; 8 | using Common.Interfaces; 9 | using Common.Models; 10 | using Domain.Entities; 11 | using MediatR; 12 | using Microsoft.EntityFrameworkCore; 13 | 14 | public class GetItemDetailsQueryHandler : IRequestHandler> 15 | { 16 | private readonly IAuctionSystemDbContext context; 17 | private readonly IMapper mapper; 18 | 19 | public GetItemDetailsQueryHandler(IAuctionSystemDbContext context, IMapper mapper) 20 | { 21 | this.context = context; 22 | this.mapper = mapper; 23 | } 24 | 25 | public async Task> Handle(GetItemDetailsQuery request, 26 | CancellationToken cancellationToken) 27 | { 28 | var item = await this.context 29 | .Items 30 | .ProjectTo(this.mapper.ConfigurationProvider) 31 | .SingleOrDefaultAsync(i => i.Id == request.Id, cancellationToken); 32 | 33 | if (item == null) 34 | { 35 | throw new NotFoundException(nameof(Item)); 36 | } 37 | 38 | return new Response(item); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/utils/hooks/useCounter.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import moment from "moment"; 3 | 4 | export function useCounter(props) { 5 | const [counter, setCounter] = useState(null); 6 | const currentTime = moment().toISOString("dd/mm/yyyy HH:mm"); 7 | const currentUtcTime = moment().utc().toISOString("dd/mm/yyyy HH:mm"); 8 | 9 | useEffect(() => { 10 | let duration; 11 | if (props.startTime > currentTime) { 12 | // Calculate remaining time until auction start 13 | let eventTime = moment.utc(props.startTime).local(); 14 | duration = moment.duration(eventTime.diff(currentTime)); 15 | } else if (props.startTime < currentTime && props.endTime > currentTime) { 16 | let eventTime = moment.utc(props.endTime).local(); 17 | duration = moment.duration(eventTime.diff(currentTime)); 18 | } 19 | 20 | const interval = 1000; 21 | 22 | let timeout; 23 | if (!counter) { 24 | timeout = setTimeout(() => { 25 | setCounter(duration); 26 | }, 500); 27 | } 28 | 29 | const timer = 30 | counter && 31 | setInterval(() => { 32 | if (counter.milliseconds() < 0 && duration < 0) { 33 | return clearInterval(timer); 34 | } 35 | duration = moment.duration(duration - interval, "milliseconds"); 36 | setCounter(duration); 37 | }, interval); 38 | return () => { 39 | clearInterval(timer); 40 | clearTimeout(timeout); 41 | }; 42 | }, [counter, props, currentTime]); 43 | 44 | return { 45 | counter, 46 | currentUtcTime, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spaweb", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@aspnet/signalr": "^1.1.4", 7 | "@fortawesome/fontawesome-svg-core": "^1.2.28", 8 | "@fortawesome/free-solid-svg-icons": "^5.13.0", 9 | "@fortawesome/react-fontawesome": "^0.1.9", 10 | "@testing-library/jest-dom": "^4.2.4", 11 | "@testing-library/react": "^9.5.0", 12 | "@testing-library/user-event": "^7.2.1", 13 | "axios": "^0.21.1", 14 | "bootstrap": "^4.4.1", 15 | "moment": "^2.26.0", 16 | "moment-timezone": "^0.5.31", 17 | "react": "^16.13.1", 18 | "react-bootstrap": "^1.0.1", 19 | "react-card-carousel": "^1.1.3", 20 | "react-datepicker": "^2.16.0", 21 | "react-dom": "^16.13.1", 22 | "react-flip-move": "^3.0.4", 23 | "react-hook-form": "^5.7.2", 24 | "react-image-gallery": "^1.0.7", 25 | "react-input-range": "^1.3.0", 26 | "react-paginate": "^6.3.2", 27 | "react-router-dom": "^5.2.0", 28 | "react-scripts": "3.4.1", 29 | "react-slugify": "^2.0.1", 30 | "react-toastify": "^6.0.3" 31 | }, 32 | "scripts": { 33 | "start": "react-scripts start", 34 | "build": "react-scripts build", 35 | "test": "react-scripts test", 36 | "eject": "react-scripts eject" 37 | }, 38 | "eslintConfig": { 39 | "extends": "react-app" 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.2%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version" 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Core/Application/Pictures/Queries/GetPictureDetailsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Pictures.Queries 2 | { 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using AutoMapper; 7 | using AutoMapper.QueryableExtensions; 8 | using Common.Exceptions; 9 | using Common.Interfaces; 10 | using Common.Models; 11 | using Domain.Entities; 12 | using MediatR; 13 | using Microsoft.EntityFrameworkCore; 14 | 15 | public class 16 | GetPictureDetailsQueryHandler : IRequestHandler> 17 | { 18 | private readonly IAuctionSystemDbContext context; 19 | private readonly IMapper mapper; 20 | 21 | public GetPictureDetailsQueryHandler(IAuctionSystemDbContext context, IMapper mapper) 22 | { 23 | this.context = context; 24 | this.mapper = mapper; 25 | } 26 | 27 | public async Task> Handle(GetPictureDetailsQuery request, 28 | CancellationToken cancellationToken) 29 | { 30 | var picture = await this.context 31 | .Pictures 32 | .Where(p => p.Id == request.Id) 33 | .ProjectTo(this.mapper.ConfigurationProvider) 34 | .SingleOrDefaultAsync(cancellationToken); 35 | 36 | if (picture == null) 37 | { 38 | throw new NotFoundException(nameof(Picture)); 39 | } 40 | 41 | return new Response(picture); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/Core/Application/Items/Commands/CreateItem/CreateItemCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Items.Commands.CreateItem 2 | { 3 | using FluentValidation; 4 | using global::Common; 5 | 6 | public class CreateItemCommandValidator : AbstractValidator 7 | { 8 | private readonly IDateTime dateTime; 9 | 10 | public CreateItemCommandValidator(IDateTime dateTime) 11 | { 12 | this.dateTime = dateTime; 13 | 14 | this.RuleFor(p => p.Title).NotEmpty().MaximumLength(ModelConstants.Item.TitleMaxLength); 15 | this.RuleFor(p => p.Description).NotEmpty().MaximumLength(ModelConstants.Item.DescriptionMaxLength); 16 | this.RuleFor(p => p.StartingPrice).NotEmpty() 17 | .InclusiveBetween(ModelConstants.Item.MinStartingPrice, ModelConstants.Item.MaxStartingPrice); 18 | this.RuleFor(p => p.MinIncrease).NotEmpty() 19 | .InclusiveBetween(ModelConstants.Item.MinMinIncrease, ModelConstants.Item.MaxMinIncrease); 20 | 21 | this.RuleFor(p => p.StartTime).NotEmpty(); 22 | this.RuleFor(p => p.EndTime).NotEmpty(); 23 | 24 | this.RuleFor(m => new { m.StartTime, m.EndTime }).NotEmpty() 25 | .Must(x => x.EndTime.Date.ToUniversalTime() >= x.StartTime.Date.ToUniversalTime()) 26 | .WithMessage("End time must be after start time") 27 | .Must(x => x.StartTime.ToUniversalTime() >= this.dateTime.Now.ToUniversalTime()) 28 | .WithMessage("The Start time must be after the current time"); 29 | 30 | this.RuleFor(p => p.SubCategoryId).NotEmpty(); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | namespace Persistence 2 | { 3 | using Application.Common.Interfaces; 4 | using AuctionSystem.Infrastructure.Identity; 5 | using Domain.Entities; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | 11 | public static class DependencyInjection 12 | { 13 | public static IServiceCollection AddPersistence(this IServiceCollection services, IConfiguration configuration) 14 | { 15 | services 16 | .AddDbContext(options => 17 | options.UseSqlServer(configuration.GetDefaultConnectionString())) 18 | .AddIdentity(options => 19 | { 20 | options.Password.RequireDigit = false; 21 | options.Password.RequireLowercase = false; 22 | options.Password.RequireNonAlphanumeric = false; 23 | options.Password.RequireUppercase = false; 24 | 25 | options.SignIn.RequireConfirmedEmail = true; 26 | options.Lockout.MaxFailedAccessAttempts = 6; 27 | }) 28 | .AddTokenProvider(FourDigitTokenProvider.FourDigitEmail) 29 | .AddEntityFrameworkStores() 30 | .AddDefaultTokenProviders(); 31 | 32 | services.AddScoped(provider => provider.GetService()); 33 | return services; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/Core/Application/Items/Commands/UpdateItem/UpdateItemCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Items.Commands.UpdateItem 2 | { 3 | using FluentValidation; 4 | using global::Common; 5 | 6 | public class UpdateItemCommandValidator : AbstractValidator 7 | { 8 | private readonly IDateTime dateTime; 9 | 10 | public UpdateItemCommandValidator(IDateTime dateTime) 11 | { 12 | this.dateTime = dateTime; 13 | 14 | this.RuleFor(p => p.Id).NotEmpty(); 15 | this.RuleFor(p => p.Title).NotEmpty().MaximumLength(ModelConstants.Item.TitleMaxLength); 16 | this.RuleFor(p => p.Description).NotEmpty().MaximumLength(ModelConstants.Item.DescriptionMaxLength); 17 | this.RuleFor(p => p.StartingPrice).NotEmpty() 18 | .InclusiveBetween(ModelConstants.Item.MinStartingPrice, ModelConstants.Item.MaxStartingPrice); 19 | this.RuleFor(p => p.MinIncrease).NotEmpty() 20 | .InclusiveBetween(ModelConstants.Item.MinMinIncrease, ModelConstants.Item.MaxMinIncrease); 21 | 22 | this.RuleFor(p => p.StartTime).NotEmpty(); 23 | this.RuleFor(p => p.EndTime).NotEmpty(); 24 | 25 | this.RuleFor(m => new { m.StartTime, m.EndTime }).NotEmpty() 26 | .Must(x => x.EndTime.Date.ToUniversalTime() >= x.StartTime.Date.ToUniversalTime()) 27 | .WithMessage("End time must be after start time") 28 | .Must(x => x.StartTime.ToUniversalTime() >= this.dateTime.Now.ToUniversalTime()) 29 | .WithMessage("The Start time must be after the current time"); 30 | 31 | this.RuleFor(p => p.SubCategoryId).NotEmpty(); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Items/List/Header/Header.css: -------------------------------------------------------------------------------- 1 | .generic-header { 2 | background: #fff; 3 | margin-bottom: 2rem; 4 | border: 1px solid #e3e6ef; 5 | display: -webkit-box; 6 | display: -webkit-flex; 7 | display: -ms-flexbox; 8 | display: flex; 9 | -webkit-box-align: center; 10 | -webkit-align-items: center; 11 | -ms-flex-align: center; 12 | align-items: center; 13 | -webkit-box-pack: justify; 14 | -webkit-justify-content: space-between; 15 | -ms-flex-pack: justify; 16 | justify-content: space-between; 17 | -webkit-flex-wrap: wrap; 18 | -ms-flex-wrap: wrap; 19 | flex-wrap: wrap; 20 | padding: 1.66667rem 2rem; 21 | } 22 | 23 | .generic-header-toolbar { 24 | display: -webkit-box; 25 | display: -webkit-flex; 26 | display: -ms-flexbox; 27 | display: flex; 28 | -webkit-flex-wrap: wrap; 29 | -ms-flex-wrap: wrap; 30 | flex-wrap: wrap; 31 | -webkit-box-pack: start; 32 | -webkit-justify-content: flex-start; 33 | -ms-flex-pack: start; 34 | justify-content: flex-start; 35 | } 36 | 37 | .generic-header .sort-btn { 38 | display: -webkit-inline-box; 39 | display: -webkit-inline-flex; 40 | display: -ms-inline-flexbox; 41 | display: inline-flex; 42 | -webkit-box-align: center; 43 | -webkit-align-items: center; 44 | -ms-flex-align: center; 45 | align-items: center; 46 | -webkit-box-pack: center; 47 | -webkit-justify-content: center; 48 | -ms-flex-pack: center; 49 | justify-content: center; 50 | border: 1px solid #e3e6ef; 51 | background-color: transparent; 52 | font-size: 0.86667rem; 53 | padding: 0 1rem; 54 | -webkit-box-shadow: none; 55 | box-shadow: none; 56 | -webkit-border-radius: 2px; 57 | border-radius: 2px; 58 | height: 2.66667rem; 59 | } 60 | -------------------------------------------------------------------------------- /src/Core/Application/Admin/Commands/CreateAdmin/CreateAdminCommandHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Admin.Commands.CreateAdmin 2 | { 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Common.Exceptions; 6 | using Common.Interfaces; 7 | using Common.Models; 8 | using MediatR; 9 | 10 | public class CreateAdminCommandHandler : IRequestHandler 11 | { 12 | private readonly IUserManager userManager; 13 | private readonly ICurrentUserService currentUserService; 14 | 15 | public CreateAdminCommandHandler(IUserManager userManager, ICurrentUserService currentUserService) 16 | { 17 | this.userManager = userManager; 18 | this.currentUserService = currentUserService; 19 | } 20 | 21 | public async Task Handle(CreateAdminCommand request, CancellationToken cancellationToken) 22 | { 23 | //TODO: Add User as default role in db 24 | if (!request.Role.Equals(AppConstants.AdministratorRole)) 25 | { 26 | throw new BadRequestException(ExceptionMessages.Admin.InvalidRole); 27 | } 28 | 29 | var result = 30 | await this.userManager.AddToRoleAsync(request.Email, request.Role, this.currentUserService.UserId); 31 | if (!result.Succeeded && result.ErrorType == ErrorType.General) 32 | { 33 | throw new BadRequestException(result.Error); 34 | } 35 | 36 | if (!result.Succeeded && result.ErrorType == ErrorType.TokenExpired) 37 | { 38 | throw new UnauthorizedException(); 39 | } 40 | 41 | return Unit.Value; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Items/DeleteModal.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Modal, Button, Spinner } from "react-bootstrap"; 3 | import itemsService from "../../services/itemsService"; 4 | import { history } from "../.."; 5 | import { toast } from "react-toastify"; 6 | 7 | export const DeleteModal = ({ show, handleClose, item }) => { 8 | const [isLoading, setIsLoading] = useState(false); 9 | 10 | const handleDelete = (id) => { 11 | setIsLoading(true); 12 | itemsService.deleteItem(id).then(() => { 13 | toast.success("Delete operation was successful"); 14 | setIsLoading(false); 15 | history.push("/"); 16 | }); 17 | }; 18 | 19 | return ( 20 | 21 | 22 | 23 | Are you sure you want to delete {item?.title}?{" "} 24 | This operation is irreversible! 25 | 26 | 27 | 28 | 31 | {isLoading ? ( 32 | 42 | ) : ( 43 | 49 | )} 50 | 51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/Core/Application/Items/Commands/DeleteItem/DeleteItemCommandHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Items.Commands.DeleteItem 2 | { 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Common.Exceptions; 6 | using Common.Interfaces; 7 | using Domain.Entities; 8 | using MediatR; 9 | using Notifications.Models; 10 | 11 | public class DeleteItemCommandHandler : IRequestHandler 12 | { 13 | private readonly IAuctionSystemDbContext context; 14 | private readonly ICurrentUserService currentUserService; 15 | private readonly IMediator mediator; 16 | 17 | public DeleteItemCommandHandler(IAuctionSystemDbContext context, 18 | ICurrentUserService currentUserService, 19 | IMediator mediator) 20 | { 21 | this.context = context; 22 | this.currentUserService = currentUserService; 23 | this.mediator = mediator; 24 | } 25 | 26 | public async Task Handle(DeleteItemCommand request, CancellationToken cancellationToken) 27 | { 28 | var itemToDelete = await this.context 29 | .Items 30 | .FindAsync(request.Id); 31 | 32 | if (itemToDelete == null 33 | || itemToDelete.UserId != this.currentUserService.UserId && !this.currentUserService.IsAdmin) 34 | { 35 | throw new NotFoundException(nameof(Item)); 36 | } 37 | 38 | this.context.Items.Remove(itemToDelete); 39 | await this.context.SaveChangesAsync(cancellationToken); 40 | await this.mediator.Publish(new ItemDeletedNotification(itemToDelete.Id), cancellationToken); 41 | 42 | return Unit.Value; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Items/Details/UserActionsContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, Fragment } from "react"; 2 | import { Card, Button } from "react-bootstrap"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import { faEdit, faTrash } from "@fortawesome/free-solid-svg-icons"; 5 | import { DeleteModal } from "../DeleteModal"; 6 | import { itemEditSlug } from "../../../utils/helpers/slug"; 7 | import { history } from "../../.."; 8 | 9 | export const UserActionsContainer = ({ userId, item }) => { 10 | const [showModal, setShowModal] = useState(false); 11 | 12 | const handleClose = () => setShowModal(false); 13 | const handleShow = () => setShowModal(true); 14 | 15 | return userId === item.userId ? ( 16 | 17 | 18 | Actions 19 |
20 | 31 |
32 |
33 | 39 |
40 |
41 | 42 |
43 | ) : ( 44 | "" 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Account/ConfirmAccount.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Form, Button } from "react-bootstrap"; 3 | import { useForm } from "react-hook-form"; 4 | import { history } from "../.."; 5 | import { toast } from "react-toastify"; 6 | 7 | export const ConfirmAccount = ({ auth, email }) => { 8 | const { register, handleSubmit } = useForm(); 9 | 10 | const onSubmit = (data) => { 11 | auth.confirmAccount(data).then(() => { 12 | history.push("/"); 13 | toast.success( 14 | "Your account has been confirmed successfully. You're now able to sign in." 15 | ); 16 | }); 17 | }; 18 | 19 | return ( 20 |
21 |
29 | 30 | Please check your email for the verification code. 31 | 32 | 33 | Verification Code - 4 digits 34 | 39 | 40 | 48 | 51 |
52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/Core/Application/Admin/Commands/DeleteAdmin/DeleteAdminCommandHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Admin.Commands.DeleteAdmin 2 | { 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Common.Exceptions; 7 | using Common.Interfaces; 8 | using Common.Models; 9 | using MediatR; 10 | 11 | public class DeleteAdminCommandHandler : IRequestHandler 12 | { 13 | private readonly ICurrentUserService currentUserService; 14 | private readonly IUserManager userManager; 15 | 16 | public DeleteAdminCommandHandler(ICurrentUserService currentUserService, IUserManager userManager) 17 | { 18 | this.currentUserService = currentUserService; 19 | this.userManager = userManager; 20 | } 21 | 22 | public async Task Handle(DeleteAdminCommand request, CancellationToken cancellationToken) 23 | { 24 | //TODO: Add User as default role in db 25 | if (!request.Role.Equals(AppConstants.AdministratorRole)) 26 | { 27 | throw new BadRequestException(ExceptionMessages.Admin.InvalidRole); 28 | } 29 | 30 | var result = 31 | await this.userManager.RemoveFromRoleAsync(request.Email, request.Role, this.currentUserService.UserId, 32 | cancellationToken); 33 | if (!result.Succeeded && result.ErrorType == ErrorType.General) 34 | { 35 | throw new BadRequestException(result.Error); 36 | } 37 | 38 | if (!result.Succeeded && result.ErrorType == ErrorType.TokenExpired) 39 | { 40 | throw new UnauthorizedException(); 41 | } 42 | 43 | return Unit.Value; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Core/Application/Common/Behaviours/RequestPerformanceBehaviour.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Behaviours 2 | { 3 | using System.Diagnostics; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Interfaces; 7 | using MediatR; 8 | using Microsoft.Extensions.Logging; 9 | 10 | public class RequestPerformanceBehaviour : IPipelineBehavior 11 | { 12 | private const int MaxResponseTime = 500; 13 | private readonly ICurrentUserService currentUserService; 14 | private readonly ILogger logger; 15 | 16 | private readonly Stopwatch timer; 17 | 18 | public RequestPerformanceBehaviour(ICurrentUserService currentUserService, ILogger logger) 19 | { 20 | this.currentUserService = currentUserService; 21 | this.logger = logger; 22 | this.timer = new Stopwatch(); 23 | } 24 | 25 | 26 | public async Task Handle( 27 | TRequest request, 28 | CancellationToken cancellationToken, 29 | RequestHandlerDelegate next) 30 | { 31 | this.timer.Start(); 32 | 33 | var response = await next(); 34 | 35 | this.timer.Stop(); 36 | 37 | if (this.timer.ElapsedMilliseconds <= MaxResponseTime) 38 | { 39 | return response; 40 | } 41 | 42 | var name = typeof(TRequest).Name; 43 | 44 | this.logger.LogWarning( 45 | "Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) @userId {@UserId} {@Request}", 46 | name, this.timer.ElapsedMilliseconds, this.currentUserService.UserId, request); 47 | 48 | return response; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/Core/Common/ModelConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Common 2 | { 3 | public static class ModelConstants 4 | { 5 | public static class User 6 | { 7 | public const string EmailRegex = 8 | @"^(?("")("".+?(?>(); 41 | result 42 | .Data 43 | .Id 44 | .Should() 45 | .Be(expectedModel.Id); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/Core/Application/Users/Commands/LoginUser/LoginUserCommandHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Users.Commands.LoginUser 2 | { 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Common; 7 | using Common.Exceptions; 8 | using Common.Interfaces; 9 | using Common.Models; 10 | using Jwt; 11 | using MediatR; 12 | 13 | public class LoginUserCommandHandler : IRequestHandler> 14 | { 15 | private readonly IMediator mediator; 16 | private readonly IUserManager userManager; 17 | private readonly IEmailSender emailSender; 18 | 19 | public LoginUserCommandHandler(IUserManager userManager, IMediator mediator, IEmailSender emailSender) 20 | { 21 | this.userManager = userManager; 22 | this.mediator = mediator; 23 | this.emailSender = emailSender; 24 | } 25 | 26 | public async Task> Handle(LoginUserCommand request, 27 | CancellationToken cancellationToken) 28 | { 29 | var (result, userId) = await this.userManager.SignIn(request.Email, request.Password); 30 | if (!result.Succeeded && result.ErrorType == ErrorType.AccountNotConfirmed) 31 | { 32 | var token = await this.userManager.GenerateEmailConfirmationCode(request.Email); 33 | await this.emailSender.SendConfirmationEmail(request.Email, token); 34 | } 35 | 36 | if (!result.Succeeded && result.ErrorType == ErrorType.General) 37 | { 38 | throw new BadRequestException(result.Error); 39 | } 40 | 41 | var model = await this.mediator 42 | .Send(new GenerateJwtTokenCommand(userId, request.Email), cancellationToken); 43 | return new Response(model); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Migrations/20200622181552_AddSetNullDeleteBehaviourForItemIdColumn.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Persistence.Migrations 5 | { 6 | public partial class AddSetNullDeleteBehaviourForItemIdColumn : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.DropForeignKey( 11 | name: "FK_Bids_Items_ItemId", 12 | table: "Bids"); 13 | 14 | migrationBuilder.AlterColumn( 15 | name: "ItemId", 16 | table: "Bids", 17 | nullable: true, 18 | oldClrType: typeof(Guid), 19 | oldType: "uniqueidentifier"); 20 | 21 | migrationBuilder.AddForeignKey( 22 | name: "FK_Bids_Items_ItemId", 23 | table: "Bids", 24 | column: "ItemId", 25 | principalTable: "Items", 26 | principalColumn: "Id", 27 | onDelete: ReferentialAction.SetNull); 28 | } 29 | 30 | protected override void Down(MigrationBuilder migrationBuilder) 31 | { 32 | migrationBuilder.DropForeignKey( 33 | name: "FK_Bids_Items_ItemId", 34 | table: "Bids"); 35 | 36 | migrationBuilder.AlterColumn( 37 | name: "ItemId", 38 | table: "Bids", 39 | type: "uniqueidentifier", 40 | nullable: false, 41 | oldClrType: typeof(Guid), 42 | oldNullable: true); 43 | 44 | migrationBuilder.AddForeignKey( 45 | name: "FK_Bids_Items_ItemId", 46 | table: "Bids", 47 | column: "ItemId", 48 | principalTable: "Items", 49 | principalColumn: "Id", 50 | onDelete: ReferentialAction.Restrict); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Presentation/Api/Controllers/BidsController.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Controllers 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using Application.Bids.Commands.CreateBid; 6 | using Application.Bids.Queries.Details; 7 | using Microsoft.AspNetCore.Authorization; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.AspNetCore.Mvc; 10 | using SwaggerExamples; 11 | using Swashbuckle.AspNetCore.Annotations; 12 | 13 | [Authorize] 14 | public class BidsController : BaseController 15 | { 16 | [HttpPost] 17 | [SwaggerResponse(StatusCodes.Status204NoContent, 18 | SwaggerDocumentation.BidConstants.SuccessfulPostRequestDescriptionMessage)] 19 | [SwaggerResponse( 20 | StatusCodes.Status400BadRequest, 21 | SwaggerDocumentation.BidConstants.BadRequestOnPostRequestDescriptionMessage, 22 | typeof(BadRequestErrorModel))] 23 | [SwaggerResponse( 24 | StatusCodes.Status401Unauthorized, 25 | SwaggerDocumentation.UnauthorizedDescriptionMessage)] 26 | [SwaggerResponse( 27 | StatusCodes.Status404NotFound, 28 | SwaggerDocumentation.BidConstants.NotFoundOnPostRequestDescriptionMessage, 29 | typeof(NotFoundErrorModel))] 30 | public async Task Post([FromBody] CreateBidCommand model) 31 | { 32 | await this.Mediator.Send(model); 33 | return this.NoContent(); 34 | } 35 | 36 | [HttpGet] 37 | [Route("getHighestBid/{itemId?}")] 38 | [SwaggerResponse( 39 | StatusCodes.Status200OK, 40 | SwaggerDocumentation.BidConstants.GetHighestBidDescriptionMessage, 41 | typeof(GetHighestBidDetailsResponseModel))] 42 | public async Task GetHighestBid(Guid itemId) 43 | { 44 | var result = await this.Mediator.Send(new GetHighestBidDetailsQuery(itemId)); 45 | return this.Ok(result); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/Presentation/Api/Services/Hosted/MigrateDatabaseHostedService.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Services.Hosted 2 | { 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Application.SeedSampleData; 7 | using MediatR; 8 | using Microsoft.EntityFrameworkCore; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | using Microsoft.Extensions.Logging; 12 | using Persistence; 13 | 14 | public class MigrateDatabaseHostedService : IHostedService 15 | { 16 | private readonly IServiceProvider serviceProvider; 17 | 18 | public MigrateDatabaseHostedService(IServiceProvider serviceProvider) 19 | { 20 | this.serviceProvider = serviceProvider; 21 | } 22 | 23 | public async Task StartAsync(CancellationToken cancellationToken) 24 | { 25 | using var scope = serviceProvider.CreateScope(); 26 | var logger = scope.ServiceProvider.GetRequiredService>(); 27 | var services = scope.ServiceProvider; 28 | try 29 | { 30 | var auctionSystemDbContext = services.GetRequiredService(); 31 | await auctionSystemDbContext.Database.MigrateAsync(cancellationToken); 32 | logger.LogInformation("Migrated database."); 33 | 34 | var mediator = services.GetRequiredService(); 35 | logger.LogInformation("Seeding sample data such as items, categories and etc."); 36 | await mediator.Send(new SeedSampleDataCommand(), CancellationToken.None); 37 | logger.LogInformation("Database seeding was successful."); 38 | } 39 | catch (Exception ex) 40 | { 41 | logger.LogError(ex, "An error occurred while migrating or initializing the database."); 42 | } 43 | } 44 | 45 | public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; 46 | } 47 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Items/Details/Details.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useEffect, useState } from "react"; 2 | import { Row, Col, Button, Card } from "react-bootstrap"; 3 | import ImageGallery from "react-image-gallery"; 4 | import itemsService from "../../../services/itemsService"; 5 | import { useParams } from "react-router-dom"; 6 | import "moment-timezone"; 7 | import { SideBar } from "./SideBar"; 8 | import "./Details.css"; 9 | import createImageObj from "../../../services/picturesService"; 10 | 11 | export const Details = () => { 12 | const [isLoading, setIsLoading] = useState(false); 13 | const [item, setItem] = useState({}); 14 | const [images, setImages] = useState([]); 15 | 16 | let { id } = useParams(); 17 | 18 | useEffect(() => { 19 | setIsLoading(true); 20 | itemsService.getItemById(id).then(({ data: response }) => { 21 | setItem(response.data); 22 | setImages(createImageObj(response.data.pictures)); 23 | setIsLoading(false); 24 | }); 25 | }, [id]); 26 | 27 | return ( 28 | 29 | {!isLoading ? ( 30 | 31 | 32 |

{item.title}

33 | 34 | 35 | 36 | 37 | 38 | 39 | {item.description} 40 |
Starting Bid
41 | €{item.startingPrice} 42 |
43 |
44 | 45 | 51 | 52 |
53 | ) : ( 54 | "Loading..." 55 | )} 56 |
57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/Presentation/Api/SwaggerExamples/Responses/Categories/ListCategoriesSuccessfulResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Api.SwaggerExamples.Responses.Categories 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Application.Categories.Queries.List; 6 | using Application.Common.Models; 7 | using Swashbuckle.AspNetCore.Filters; 8 | 9 | public class ListCategoriesSuccessfulResponse : IExamplesProvider> 10 | { 11 | public MultiResponse GetExamples() 12 | => new MultiResponse( 13 | new List 14 | { 15 | new ListCategoriesResponseModel 16 | { 17 | Id = Guid.NewGuid(), 18 | Name = "Art", 19 | SubCategories = new List 20 | { 21 | new SubCategoriesDto { Id = Guid.NewGuid(), Name = "Drawings" }, 22 | new SubCategoriesDto { Id = Guid.NewGuid(), Name = "Photography" }, 23 | new SubCategoriesDto { Id = Guid.NewGuid(), Name = "Sculptures" } 24 | } 25 | }, 26 | new ListCategoriesResponseModel 27 | { 28 | Id = Guid.NewGuid(), 29 | Name = "Jewelry", 30 | SubCategories = new List 31 | { 32 | new SubCategoriesDto { Id = Guid.NewGuid(), Name = "Necklaces & Pendants" }, 33 | new SubCategoriesDto { Id = Guid.NewGuid(), Name = "Brooches & Pins" }, 34 | new SubCategoriesDto { Id = Guid.NewGuid(), Name = "Earrings" }, 35 | new SubCategoriesDto { Id = Guid.NewGuid(), Name = "Rings" } 36 | } 37 | } 38 | }); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Core/Application/ExceptionMessages.cs: -------------------------------------------------------------------------------- 1 | namespace Application 2 | { 3 | public static class ExceptionMessages 4 | { 5 | public static class Admin 6 | { 7 | public const string InvalidRole = "Invalid role."; 8 | public const string UserNotAddedSuccessfullyToRole = "Something went wrong while adding user to {0} role!"; 9 | public const string CannotRemoveSelfFromRole = "You can not remove yourself from role {0}!"; 10 | public const string NotInRole = "{0} is not {1}."; 11 | 12 | public const string UserNotRemovedSuccessfullyFromRole = 13 | "Something went wrong while removing user from {0} role!"; 14 | } 15 | 16 | public static class Bid 17 | { 18 | public const string InvalidBidAmount = "Invalid bid amount!"; 19 | public const string BiddingNotStartedYet = "Biding for item {0} has not started yet!"; 20 | public const string BiddingHasEnded = "Bidding for item {0} has ended."; 21 | } 22 | 23 | public static class Item 24 | { 25 | public const string CreateItemErrorMessage = "An error occured while creating item."; 26 | public const string SubCategoryDoesNotExist = "Subcategory does not exist!"; 27 | } 28 | 29 | public static class User 30 | { 31 | public const string UserNotCreatedSuccessfully = "User was not created successfully!"; 32 | public const string InvalidCredentials = "Invalid credentials!"; 33 | public const string InvalidRefreshToken = "Invalid token"; 34 | public const string UserNotFound = "User not found"; 35 | 36 | public const string AccountLockout = 37 | "Your account has been locked out due to too many invalid login attempts. Please try again."; 38 | 39 | public const string ConfirmAccount = "Please confirm your account"; 40 | public const string EmailVerificationFailed = "Account confirmation failed. Please try again later."; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Tests/Application.UnitTests/Items/Queries/GetItemDetailsQueryHandlerTests.cs: -------------------------------------------------------------------------------- 1 | namespace Application.UnitTests.Items.Queries 2 | { 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Application.Items.Queries.Details; 7 | using AutoMapper; 8 | using Common.Exceptions; 9 | using Common.Interfaces; 10 | using Common.Models; 11 | using FluentAssertions; 12 | using Setup; 13 | using Xunit; 14 | 15 | [Collection("QueryCollection")] 16 | public class GetItemDetailsQueryHandlerTests 17 | { 18 | private readonly IAuctionSystemDbContext context; 19 | private readonly IMapper mapper; 20 | 21 | public GetItemDetailsQueryHandlerTests(QueryTestFixture fixture) 22 | { 23 | this.context = fixture.Context; 24 | this.mapper = fixture.Mapper; 25 | } 26 | 27 | [Theory] 28 | [InlineData("0d0942f7-7ad3-4195-b712-c63d9a2cea30")] 29 | [InlineData("8d3cc00e-7f8d-4da8-9a85-088acf728487")] 30 | [InlineData("833eb36a-ea38-45e8-ae1c-a52caca13c56")] 31 | public async Task GetItemDetails_Given_InvalidId_Should_Throw_NotFoundException(string id) 32 | { 33 | var handler = new GetItemDetailsQueryHandler(this.context, this.mapper); 34 | await Assert.ThrowsAsync(() => 35 | handler.Handle(new GetItemDetailsQuery(Guid.Parse(id)), CancellationToken.None)); 36 | } 37 | 38 | [Fact] 39 | public async Task GetItemDetails_Should_Return_CorrectEntityAndModel() 40 | { 41 | var handler = new GetItemDetailsQueryHandler(this.context, this.mapper); 42 | var result = await handler.Handle(new GetItemDetailsQuery(DataConstants.SampleItemId), CancellationToken.None); 43 | 44 | result 45 | .Should() 46 | .BeOfType>(); 47 | result 48 | .Data 49 | .Id 50 | .Should() 51 | .Be(DataConstants.SampleItemId); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Configurations/ItemConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Persistence.Configurations 2 | { 3 | using Common; 4 | using Domain.Entities; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 7 | 8 | public class ItemConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder 13 | .ToTable("Items"); 14 | 15 | builder 16 | .HasKey(p => p.Id); 17 | 18 | builder 19 | .Property(p => p.Title) 20 | .IsRequired() 21 | .HasMaxLength(ModelConstants.Item.TitleMaxLength); 22 | 23 | builder 24 | .Property(p => p.Description) 25 | .IsRequired() 26 | .HasMaxLength(ModelConstants.Item.DescriptionMaxLength); 27 | 28 | builder 29 | .Property(p => p.StartingPrice) 30 | .IsRequired(); 31 | 32 | builder 33 | .Property(p => p.MinIncrease) 34 | .IsRequired(); 35 | 36 | builder 37 | .Property(p => p.StartTime) 38 | .IsRequired(); 39 | 40 | builder 41 | .Property(p => p.EndTime) 42 | .IsRequired(); 43 | 44 | builder 45 | .Property(p => p.IsEmailSent) 46 | .IsRequired(); 47 | 48 | builder 49 | .Property(p => p.UserId) 50 | .IsRequired(); 51 | 52 | builder 53 | .Property(p => p.SubCategoryId) 54 | .IsRequired(); 55 | 56 | builder 57 | .HasMany(b => b.Bids) 58 | .WithOne(i => i.Item) 59 | .HasForeignKey(i => i.ItemId) 60 | .OnDelete(DeleteBehavior.SetNull); 61 | 62 | builder 63 | .HasMany(b => b.Pictures) 64 | .WithOne(i => i.Item) 65 | .HasForeignKey(i => i.ItemId); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/Infrastructure/AuctionSystem.Infrastructure/Identity/FourDigitTokenProvider.cs: -------------------------------------------------------------------------------- 1 | namespace AuctionSystem.Infrastructure.Identity 2 | { 3 | using System.Globalization; 4 | using System.Threading.Tasks; 5 | using Domain.Entities; 6 | using Microsoft.AspNetCore.Identity; 7 | 8 | public class FourDigitTokenProvider : PhoneNumberTokenProvider 9 | { 10 | public const string FourDigitPhone = "4DigitPhone"; 11 | public const string FourDigitEmail = "4DigitEmail"; 12 | 13 | public override Task CanGenerateTwoFactorTokenAsync(UserManager manager, AuctionUser user) => 14 | Task.FromResult(false); 15 | 16 | public override async Task GenerateAsync(string purpose, 17 | UserManager manager, 18 | AuctionUser user) 19 | { 20 | var token = new SecurityToken(await manager.CreateSecurityTokenAsync(user)); 21 | var modifier = await this.GetUserModifierAsync(purpose, manager, user); 22 | var code = Rfc6238AuthenticationService.GenerateCode(token, modifier, 4) 23 | .ToString("D4", CultureInfo.InvariantCulture); 24 | 25 | return code; 26 | } 27 | 28 | public override async Task ValidateAsync(string purpose, 29 | string token, 30 | UserManager manager, 31 | AuctionUser user) 32 | { 33 | if (!int.TryParse(token, out var code)) 34 | { 35 | return false; 36 | } 37 | 38 | var securityToken = new SecurityToken(await manager.CreateSecurityTokenAsync(user)); 39 | var modifier = await this.GetUserModifierAsync(purpose, manager, user); 40 | var valid = Rfc6238AuthenticationService.ValidateCode(securityToken, code, modifier, token.Length); 41 | return valid; 42 | } 43 | 44 | public override Task GetUserModifierAsync(string purpose, 45 | UserManager manager, 46 | AuctionUser user) => base.GetUserModifierAsync(purpose, manager, user); 47 | } 48 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/utils/hooks/authHook.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext, createContext } from "react"; 2 | import api, { setupAxiosInterceptor } from "../helpers/api"; 3 | import { 4 | setUserInLocalStorage, 5 | removeUserFromLocalStorage, 6 | getUserFromLocalStorage, 7 | } from "../helpers/localStorage"; 8 | 9 | const authContext = createContext(); 10 | 11 | export function ProvideAuth({ children }) { 12 | const auth = useProvideAuth(); 13 | return ( 14 | 15 | {!auth.isLoading ? children : null} 16 | 17 | ); 18 | } 19 | 20 | export const useAuth = () => { 21 | return useContext(authContext); 22 | }; 23 | 24 | function useProvideAuth() { 25 | const [user, setUser] = useState(null); 26 | const [isLoading, setIsLoading] = useState(true); 27 | 28 | useEffect(() => { 29 | if (user === null) { 30 | setUser(getUserFromLocalStorage()); 31 | setIsLoading(false); 32 | } 33 | }, [user, isLoading]); 34 | 35 | useEffect(() => { 36 | setupAxiosInterceptor(signOut); 37 | }, []); 38 | 39 | const signIn = (body) => { 40 | return api 41 | .post(process.env.REACT_APP_API_LOGIN_ENDPOINT, body) 42 | .then((response) => { 43 | if (response.status === 200) { 44 | const data = setUserInLocalStorage(response); 45 | setUser(data); 46 | } 47 | 48 | return response.data; 49 | }); 50 | }; 51 | 52 | const signUp = (body) => { 53 | return api 54 | .post(process.env.REACT_APP_API_REGISTER_ENDPOINT, body) 55 | .then((response) => response); 56 | }; 57 | 58 | const confirmAccount = (body) => { 59 | return api 60 | .post(process.env.REACT_APP_API_CONFIRM_ACCOUNT_ENDPOINT, body) 61 | .then((response) => response); 62 | }; 63 | 64 | const signOut = () => { 65 | api.post(process.env.REACT_APP_API_LOGOUT_ENDPOINT, {}).then(() => { 66 | removeUserFromLocalStorage(); 67 | setUser(null); 68 | }); 69 | }; 70 | 71 | return { 72 | user, 73 | isLoading, 74 | signIn, 75 | signUp, 76 | confirmAccount, 77 | signOut, 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { NavMenu } from "./components/NavMenu"; 3 | import { Home } from "./components/Home/Home"; 4 | import { Switch, Route } from "react-router-dom"; 5 | import { Container } from "react-bootstrap"; 6 | import { ToastContainer } from "react-toastify"; 7 | import { NetworkError } from "./components/Error/NetworkError"; 8 | import { NotFound } from "./components/Error/NotFound"; 9 | import { Register } from "./components/Account/Register/Register"; 10 | import { Login } from "./components/Account/Login/Login"; 11 | import { ProvideAuth } from "./utils/hooks/authHook"; 12 | import { List } from "./components/Items/List/List"; 13 | import { Details } from "./components/Items/Details/Details"; 14 | import { Create } from "./components/Items/Create/Create"; 15 | import { PrivateRoute } from "./components/PrivateRoute"; 16 | import { Edit } from "./components/Items/Edit/Edit"; 17 | import { Admin } from "./components/Admin/Admin"; 18 | import "react-toastify/dist/ReactToastify.css"; 19 | 20 | function App() { 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | 49 | export default App; 50 | -------------------------------------------------------------------------------- /src/Presentation/Api/Hubs/BidHub.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Hubs 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using Application.Bids.Commands.CreateBid; 6 | using Application.Common.Exceptions; 7 | using Application.Common.Interfaces; 8 | using MediatR; 9 | using Microsoft.AspNetCore.Authentication.JwtBearer; 10 | using Microsoft.AspNetCore.Authorization; 11 | using Microsoft.AspNetCore.SignalR; 12 | 13 | public class BidHub : Hub 14 | { 15 | private readonly IMediator mediator; 16 | private readonly ICurrentUserService currentUserService; 17 | 18 | public BidHub(IMediator mediator, ICurrentUserService currentUserService) 19 | { 20 | this.mediator = mediator; 21 | this.currentUserService = currentUserService; 22 | } 23 | 24 | [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] 25 | public async Task Setup(string itemId) 26 | { 27 | if (itemId == null) 28 | { 29 | return; 30 | } 31 | 32 | await this.Groups.AddToGroupAsync(this.Context.ConnectionId, itemId); 33 | } 34 | 35 | [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] 36 | public async Task CreateBidAsync(decimal bidAmount, string itemId) 37 | { 38 | try 39 | { 40 | var userId = this.currentUserService.UserId; 41 | await this.mediator.Send(new CreateBidCommand 42 | { 43 | Amount = bidAmount, 44 | ItemId = Guid.Parse(itemId), 45 | UserId = userId, 46 | }); 47 | 48 | await this.Clients.Groups(itemId).SendAsync("ReceiveMessage", bidAmount, userId); 49 | } 50 | catch (NotFoundException ex) 51 | { 52 | await this.Clients.Caller.SendAsync("handleException", ex.Message); 53 | } 54 | catch (BadRequestException ex) 55 | { 56 | await this.Clients.Caller.SendAsync("handleException", ex.Message); 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Items/List/List.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState, useEffect } from "react"; 2 | import { Container, Row, Col } from "react-bootstrap"; 3 | import "react-input-range/lib/css/index.css"; 4 | import useItemsSearch from "../../../utils/hooks/useItemsSearch"; 5 | import useDebounce from "../../../utils/hooks/useDebounce"; 6 | import { Container as ItemsContainer } from "./Container/Container"; 7 | import { Search } from "./Search/Search"; 8 | import { useParams } from "react-router-dom"; 9 | import { Header } from "./Header/Header"; 10 | 11 | export const List = () => { 12 | const [state, setState] = useState({ 13 | title: null, 14 | getLiveItems: true, 15 | minPrice: null, 16 | maxPrice: null, 17 | startTime: null, 18 | endTime: null, 19 | subCategoryId: null, 20 | }); 21 | 22 | let { subCategoryId } = useParams(); 23 | 24 | const [pageNumber, setPageNumber] = useState(1); 25 | const query = useDebounce(state, 500); 26 | const { 27 | makeRequest, 28 | items, 29 | totalItemsCount, 30 | hasMore, 31 | loading, 32 | error, 33 | } = useItemsSearch(query, pageNumber, setPageNumber); 34 | 35 | useEffect(() => { 36 | if (state.subCategoryId !== subCategoryId) { 37 | setState((prev) => ({ ...prev, subCategoryId })); 38 | } else makeRequest(); 39 | // eslint-disable-next-line 40 | }, [subCategoryId, makeRequest]); 41 | 42 | return ( 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /Tests/Application.UnitTests/Pictures/Queries/GetPictureDetailsQueryHandlerTests.cs: -------------------------------------------------------------------------------- 1 | namespace Application.UnitTests.Pictures.Queries 2 | { 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Application.Pictures.Queries; 7 | using AutoMapper; 8 | using Common.Exceptions; 9 | using Common.Interfaces; 10 | using Common.Models; 11 | using FluentAssertions; 12 | using Setup; 13 | using Xunit; 14 | 15 | [Collection("QueryCollection")] 16 | public class GetPictureDetailsQueryHandlerTests 17 | { 18 | private readonly IAuctionSystemDbContext context; 19 | private readonly IMapper mapper; 20 | 21 | private readonly GetPictureDetailsQueryHandler handler; 22 | 23 | public GetPictureDetailsQueryHandlerTests(QueryTestFixture fixture) 24 | { 25 | this.context = fixture.Context; 26 | this.mapper = fixture.Mapper; 27 | 28 | this.handler = new GetPictureDetailsQueryHandler(this.context, this.mapper); 29 | } 30 | 31 | [Theory] 32 | [InlineData("16488cbf-0e07-4390-9eb5-9627796ffa29")] 33 | [InlineData("159ae2cf-7f5a-4a90-9a78-59ffe738f1a6")] 34 | [InlineData("a00004c9-a289-44c1-b425-71988da7d9f5")] 35 | [InlineData("f4b5269c-e284-4448-9013-ea62c4e9379f")] 36 | public async Task GetPictureDetails_Given_InvalidId_Should_Throw_NotFoundException(string id) 37 | => await Assert 38 | .ThrowsAsync(() => 39 | this.handler.Handle(new GetPictureDetailsQuery(Guid.Parse(id)), CancellationToken.None)); 40 | 41 | [Fact] 42 | public async Task GetPictureDetails_Given_ValidId_Should_Return_CorrectEntityAndModel() 43 | { 44 | var expectedId = DataConstants.SamplePictureId; 45 | var result = await this.handler.Handle(new GetPictureDetailsQuery(expectedId), CancellationToken.None); 46 | 47 | result 48 | .Should() 49 | .BeOfType>(); 50 | 51 | result 52 | .Data 53 | .Id 54 | .Should() 55 | .Be(expectedId); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/Presentation/SpaWeb/src/components/Home/HottestItems/HottestItems.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import ReactCardCarousel from "react-card-carousel"; 3 | import itemsService from "../../../services/itemsService"; 4 | import { Link } from "react-router-dom"; 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 6 | import { faFire } from "@fortawesome/free-solid-svg-icons"; 7 | import { Container, Card } from "react-bootstrap"; 8 | import { itemDetailsSlug } from "../../../utils/helpers/slug"; 9 | 10 | import "../HottestItems/HottestItems.css"; 11 | 12 | export const HottestItems = () => { 13 | const [items, setItems] = useState([]); 14 | useEffect(() => { 15 | retrieveHottestUpcomingItems(); 16 | }, []); 17 | 18 | const retrieveHottestUpcomingItems = () => { 19 | itemsService.getHottestUpcomingItems().then((response) => { 20 | setItems(response.data.data); 21 | }); 22 | }; 23 | 24 | return items.length !== 0 ? ( 25 | 26 |

27 | Hottest upcoming items{" "} 28 | 29 |

30 |
31 | 32 | {items.map((item, index) => { 33 | return ( 34 | 35 | 40 | 41 | {item.title} 42 |

43 | {process.env.REACT_APP_CURRENCY_SIGN} 44 | {item.startingPrice} 45 |

46 | 50 | View 51 | 52 |
53 |
54 | ); 55 | })} 56 |
57 |
58 |
59 | ) : ( 60 | "" 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /src/Core/Application/Items/Commands/CreateItem/CreateItemCommandHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Items.Commands.CreateItem 2 | { 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using AutoMapper; 6 | using Common.Exceptions; 7 | using Common.Interfaces; 8 | using Common.Models; 9 | using Domain.Entities; 10 | using MediatR; 11 | using Microsoft.EntityFrameworkCore; 12 | using Pictures.Commands.CreatePicture; 13 | 14 | public class CreateItemCommandHandler : IRequestHandler> 15 | { 16 | private readonly IAuctionSystemDbContext context; 17 | private readonly IMapper mapper; 18 | private readonly IMediator mediator; 19 | private readonly ICurrentUserService userService; 20 | 21 | public CreateItemCommandHandler(IAuctionSystemDbContext context, 22 | IMapper mapper, 23 | IMediator mediator, 24 | ICurrentUserService userService) 25 | { 26 | this.context = context; 27 | this.mapper = mapper; 28 | this.mediator = mediator; 29 | this.userService = userService; 30 | } 31 | 32 | 33 | public async Task> Handle(CreateItemCommand request, 34 | CancellationToken cancellationToken) 35 | { 36 | if (this.userService.UserId == null 37 | || !await this.context.SubCategories.AnyAsync(c => c.Id == request.SubCategoryId, cancellationToken)) 38 | { 39 | throw new BadRequestException(ExceptionMessages.Item.CreateItemErrorMessage); 40 | } 41 | 42 | var item = this.mapper.Map(request); 43 | item.UserId = this.userService.UserId; 44 | item.StartTime = item.StartTime.ToUniversalTime(); 45 | item.EndTime = item.EndTime.ToUniversalTime(); 46 | 47 | await this.context.Items.AddAsync(item, cancellationToken); 48 | await this.context.SaveChangesAsync(cancellationToken); 49 | 50 | await this.mediator.Send(new CreatePictureCommand { ItemId = item.Id, Pictures = request.Pictures }, 51 | cancellationToken); 52 | 53 | return new Response(new ItemResponseModel(item.Id)); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /Tests/Application.UnitTests/Admin/Queries/ListAllUsersQueryHandlerTests.cs: -------------------------------------------------------------------------------- 1 | namespace Application.UnitTests.Admin.Queries 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Application.Admin.Queries.List; 8 | using AuctionSystem.Infrastructure.Identity; 9 | using AutoMapper; 10 | using Common.Interfaces; 11 | using Common.Models; 12 | using Domain.Entities; 13 | using FluentAssertions; 14 | using Microsoft.AspNetCore.Identity; 15 | using Microsoft.EntityFrameworkCore; 16 | using Moq; 17 | using Setup; 18 | using Xunit; 19 | 20 | [Collection("QueryCollection")] 21 | public class ListAllUsersQueryHandlerTests 22 | { 23 | private readonly IAuctionSystemDbContext context; 24 | private readonly IMapper mapper; 25 | private readonly IUserManager userManagerService; 26 | private readonly Mock> mockedUserManager; 27 | 28 | public ListAllUsersQueryHandlerTests(QueryTestFixture fixture) 29 | { 30 | this.context = fixture.Context; 31 | this.mapper = fixture.Mapper; 32 | this.mockedUserManager = IdentityMocker.GetMockedUserManager(); 33 | 34 | this.userManagerService = new UserManagerService( 35 | this.mockedUserManager.Object, 36 | IdentityMocker.GetMockedRoleManager().Object, 37 | this.context); 38 | } 39 | 40 | [Fact] 41 | public async Task GetUsers_Should_Return_Correct_Count() 42 | { 43 | this.mockedUserManager 44 | .Setup(x => x.GetUsersInRoleAsync(AppConstants.AdministratorRole)) 45 | .ReturnsAsync(new List { new AuctionUser { Id = Guid.NewGuid().ToString() } }); 46 | var handler = new ListAllUsersQueryHandler(this.context, this.mapper, this.userManagerService); 47 | 48 | var result = await handler.Handle(new ListAllUsersQuery(), CancellationToken.None); 49 | 50 | result 51 | .Should() 52 | .BeOfType>(); 53 | 54 | result 55 | .Data 56 | .Should() 57 | .HaveCount(await this.context.Users.CountAsync()); 58 | } 59 | } 60 | } --------------------------------------------------------------------------------