├── .gitignore ├── .vs └── skinet │ ├── DesignTimeBuild │ └── .dtbcache.v2 │ ├── config │ └── applicationhost.config │ └── v16 │ ├── .suo │ └── TestStore │ └── 0 │ ├── 000.testlog │ └── testlog.manifest ├── .vscode ├── launch.json └── tasks.json ├── API ├── API.csproj ├── API.csproj.user ├── Content │ └── images │ │ └── products │ │ ├── boot-ang1.png │ │ ├── boot-ang2.png │ │ ├── boot-core1.png │ │ ├── boot-core2.png │ │ ├── boot-redis1.png │ │ ├── glove-code1.png │ │ ├── glove-code2.png │ │ ├── glove-react1.png │ │ ├── glove-react2.png │ │ ├── hat-core1.png │ │ ├── hat-react1.png │ │ ├── hat-react2.png │ │ ├── sb-ang1.png │ │ ├── sb-ang2.png │ │ ├── sb-core1.png │ │ ├── sb-core2.png │ │ ├── sb-react1.png │ │ └── sb-ts1.png ├── Controllers │ ├── AccountController.cs │ ├── BaseApiController.cs │ ├── BasketController.cs │ ├── BuggyController.cs │ ├── ErrorController.cs │ ├── FallbackController.cs │ ├── OrdersController.cs │ ├── PaymentsController.cs │ └── ProductsController.cs ├── Dtos │ ├── AddressDto.cs │ ├── BasketItemDto.cs │ ├── CustomerBasketDto.cs │ ├── LoginDto.cs │ ├── OrderDto.cs │ ├── OrderItemDto.cs │ ├── OrderToReturnDto.cs │ ├── ProductToReturnDto.cs │ ├── RegisterDto.cs │ └── UserDto.cs ├── Errors │ ├── ApiException.cs │ ├── ApiResponse.cs │ └── ApiValidationErrorResponse.cs ├── Extensions │ ├── ApplicationServicesExtensions.cs │ ├── ClaimsPrincipalExtension.cs │ ├── IdentityServiceExtension.cs │ ├── SwaggerServiceExtension.cs │ └── UserManagerExtension.cs ├── Helpers │ ├── CachedAttribute.cs │ ├── MappingProfiles.cs │ ├── OrderItemUrlResolver.cs │ ├── Pagination.cs │ └── ProductUrlResolver.cs ├── Middleware │ └── ExceptionMiddleware.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── appsettings.Development.json └── wwwroot │ ├── 10-es2015.673eea9902d6d1a9c319.js │ ├── 10-es5.673eea9902d6d1a9c319.js │ ├── 3rdpartylicenses.txt │ ├── 6-es2015.dcbbd0451b5c32d9c498.js │ ├── 6-es5.dcbbd0451b5c32d9c498.js │ ├── 7-es2015.13d3f47c9e301321bae0.js │ ├── 7-es5.13d3f47c9e301321bae0.js │ ├── 8-es2015.ecc3cf4fe5764b865c78.js │ ├── 8-es5.ecc3cf4fe5764b865c78.js │ ├── 9-es2015.9567b13ee8b353b706b0.js │ ├── 9-es5.9567b13ee8b353b706b0.js │ ├── assets │ └── images │ │ ├── hero1.jpg │ │ ├── hero2.jpg │ │ ├── hero3.jpg │ │ ├── logo.png │ │ └── placeholder.png │ ├── common-es2015.4bde711c6da773549531.js │ ├── common-es5.4bde711c6da773549531.js │ ├── favicon.ico │ ├── fontawesome-webfont.1e59d2330b4c6deb84b3.ttf │ ├── fontawesome-webfont.20fd1704ea223900efa9.woff2 │ ├── fontawesome-webfont.8b43027f47b20503057d.eot │ ├── fontawesome-webfont.c1e38fd9e0e74ba58f7a.svg │ ├── fontawesome-webfont.f691f37e57f04c152e23.woff │ ├── index.html │ ├── main-es2015.58da4c8307c6b61d1405.js │ ├── main-es5.58da4c8307c6b61d1405.js │ ├── polyfills-es2015.ae2ff135b6a0a9cb55e7.js │ ├── polyfills-es5.736f2d9faec4ce67a92c.js │ ├── runtime-es2015.597c9a66078d39006feb.js │ ├── runtime-es5.597c9a66078d39006feb.js │ └── styles.3547713b7f8ed273ef97.css ├── Core ├── Core.csproj ├── Entities │ ├── BaseEntity.cs │ ├── BasketItem.cs │ ├── CustomerBasket.cs │ ├── Identity │ │ ├── Address.cs │ │ └── AppUser.cs │ ├── OrderAggregate │ │ ├── Address.cs │ │ ├── DeliveryMethod.cs │ │ ├── Order.cs │ │ ├── OrderItem.cs │ │ ├── OrderStatus.cs │ │ └── ProductItemOrdered.cs │ ├── Product.cs │ ├── ProductBrand.cs │ └── ProductType.cs ├── Interfaces │ ├── IBasketRepository.cs │ ├── IGenericRepository.cs │ ├── IOrderService.cs │ ├── IPaymentService.cs │ ├── IProductRepository.cs │ ├── IResponseCacheService.cs │ ├── ITokenService.cs │ └── IUnitOfWork.cs └── Specification │ ├── BaseSpecipication.cs │ ├── ISpecification.cs │ ├── OrderByPaymentIntentWithItemSpecification.cs │ ├── OrderWithItemsAndOrderingSpecipication.cs │ ├── ProductSpecParams.cs │ ├── ProductWithFilterForCountSpecification.cs │ └── ProductWithTypesAndBrandsSpecification.cs ├── Infrastructure ├── Data │ ├── BasketRepository.cs │ ├── Config │ │ ├── DeliveryMethodConfiguration.cs │ │ ├── OrderConfiguration.cs │ │ ├── OrderItemConfiguration.cs │ │ └── ProductConfiguration.cs │ ├── GenericRepository.cs │ ├── Migrations │ │ ├── 20200807140219_MYSQL Initial.Designer.cs │ │ ├── 20200807140219_MYSQL Initial.cs │ │ └── StoreContextModelSnapshot.cs │ ├── ProductRepository.cs │ ├── SeedData │ │ ├── brands.json │ │ ├── delivery.json │ │ ├── products.json │ │ └── types.json │ ├── SpecificationEvaluator.cs │ ├── StoreContext.cs │ ├── StoreContextSeed.cs │ └── UnitOfWork.cs ├── Identity │ ├── AppIdentityDbContext.cs │ └── AppIdentityDbContextSeed.cs ├── Infrastructure.csproj └── Services │ ├── OrderService.cs │ ├── PaymentService.cs │ ├── ResponseCacheService.cs │ └── TokenService.cs ├── README.md ├── client ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── browserslist ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── account │ │ │ ├── account-routing.module.ts │ │ │ ├── account.module.ts │ │ │ ├── account.service.ts │ │ │ ├── login │ │ │ │ ├── login.component.html │ │ │ │ ├── login.component.scss │ │ │ │ └── login.component.ts │ │ │ └── register │ │ │ │ ├── register.component.html │ │ │ │ ├── register.component.scss │ │ │ │ └── register.component.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── basket │ │ │ ├── basket-routing.module.ts │ │ │ ├── basket.component.html │ │ │ ├── basket.component.scss │ │ │ ├── basket.component.ts │ │ │ ├── basket.module.ts │ │ │ └── basket.service.ts │ │ ├── checkout │ │ │ ├── checkout-address │ │ │ │ ├── checkout-address.component.html │ │ │ │ ├── checkout-address.component.scss │ │ │ │ └── checkout-address.component.ts │ │ │ ├── checkout-delivery │ │ │ │ ├── checkout-delivery.component.html │ │ │ │ ├── checkout-delivery.component.scss │ │ │ │ └── checkout-delivery.component.ts │ │ │ ├── checkout-payment │ │ │ │ ├── checkout-payment.component.html │ │ │ │ ├── checkout-payment.component.scss │ │ │ │ └── checkout-payment.component.ts │ │ │ ├── checkout-review │ │ │ │ ├── checkout-review.component.html │ │ │ │ ├── checkout-review.component.scss │ │ │ │ └── checkout-review.component.ts │ │ │ ├── checkout-routing.module.ts │ │ │ ├── checkout-success │ │ │ │ ├── checkout-success.component.html │ │ │ │ ├── checkout-success.component.scss │ │ │ │ └── checkout-success.component.ts │ │ │ ├── checkout.component.html │ │ │ ├── checkout.component.scss │ │ │ ├── checkout.component.ts │ │ │ ├── checkout.module.ts │ │ │ └── checkout.service.ts │ │ ├── core │ │ │ ├── Guards │ │ │ │ └── auth.guard.ts │ │ │ ├── Services │ │ │ │ └── busy.service.ts │ │ │ ├── core.module.ts │ │ │ ├── interceptors │ │ │ │ ├── error.interceptor.ts │ │ │ │ ├── jwt.interceptor.ts │ │ │ │ └── loading.interceptor.ts │ │ │ ├── nav-bar │ │ │ │ ├── nav-bar.component.html │ │ │ │ ├── nav-bar.component.scss │ │ │ │ └── nav-bar.component.ts │ │ │ ├── not-found │ │ │ │ ├── not-found.component.html │ │ │ │ ├── not-found.component.scss │ │ │ │ └── not-found.component.ts │ │ │ ├── section-header │ │ │ │ ├── section-header.component.html │ │ │ │ ├── section-header.component.scss │ │ │ │ └── section-header.component.ts │ │ │ ├── server-error │ │ │ │ ├── server-error.component.html │ │ │ │ ├── server-error.component.scss │ │ │ │ └── server-error.component.ts │ │ │ └── test-error │ │ │ │ ├── test-error.component.html │ │ │ │ ├── test-error.component.scss │ │ │ │ └── test-error.component.ts │ │ ├── home │ │ │ ├── home.component.html │ │ │ ├── home.component.scss │ │ │ ├── home.component.ts │ │ │ └── home.module.ts │ │ ├── orders │ │ │ ├── order-detailed │ │ │ │ ├── order-detailed.component.html │ │ │ │ ├── order-detailed.component.scss │ │ │ │ └── order-detailed.component.ts │ │ │ ├── orders-routing.module.ts │ │ │ ├── orders.component.html │ │ │ ├── orders.component.scss │ │ │ ├── orders.component.ts │ │ │ ├── orders.module.ts │ │ │ └── orders.service.ts │ │ ├── shared │ │ │ ├── components │ │ │ │ ├── basket-summary │ │ │ │ │ ├── basket-summary.component.html │ │ │ │ │ ├── basket-summary.component.scss │ │ │ │ │ └── basket-summary.component.ts │ │ │ │ ├── order-totals │ │ │ │ │ ├── order-totals.component.html │ │ │ │ │ ├── order-totals.component.scss │ │ │ │ │ └── order-totals.component.ts │ │ │ │ ├── pager │ │ │ │ │ ├── pager.component.html │ │ │ │ │ ├── pager.component.scss │ │ │ │ │ └── pager.component.ts │ │ │ │ ├── paging-header │ │ │ │ │ ├── paging-header.component.html │ │ │ │ │ ├── paging-header.component.scss │ │ │ │ │ └── paging-header.component.ts │ │ │ │ └── text-input │ │ │ │ │ ├── text-input.component.html │ │ │ │ │ ├── text-input.component.scss │ │ │ │ │ └── text-input.component.ts │ │ │ ├── models │ │ │ │ ├── address.ts │ │ │ │ ├── basket.ts │ │ │ │ ├── brand.ts │ │ │ │ ├── deliveryMethod.ts │ │ │ │ ├── order.ts │ │ │ │ ├── pagination.ts │ │ │ │ ├── product.ts │ │ │ │ ├── productType.ts │ │ │ │ ├── shopParams.ts │ │ │ │ └── user.ts │ │ │ ├── shared.module.ts │ │ │ └── stepper │ │ │ │ ├── stepper.component.html │ │ │ │ ├── stepper.component.scss │ │ │ │ └── stepper.component.ts │ │ └── shop │ │ │ ├── product-details │ │ │ ├── product-details.component.html │ │ │ ├── product-details.component.scss │ │ │ └── product-details.component.ts │ │ │ ├── product-item │ │ │ ├── product-item.component.html │ │ │ ├── product-item.component.scss │ │ │ └── product-item.component.ts │ │ │ ├── shop-routing.module.ts │ │ │ ├── shop.component.html │ │ │ ├── shop.component.scss │ │ │ ├── shop.component.ts │ │ │ ├── shop.module.ts │ │ │ └── shop.service.ts │ ├── assets │ │ ├── .gitkeep │ │ └── images │ │ │ ├── hero1.jpg │ │ │ ├── hero2.jpg │ │ │ ├── hero3.jpg │ │ │ ├── logo.png │ │ │ └── placeholder.png │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json └── skinet.sln /.gitignore: -------------------------------------------------------------------------------- 1 | obj 2 | bin 3 | *.db 4 | appsettings.json -------------------------------------------------------------------------------- /.vs/skinet/DesignTimeBuild/.dtbcache.v2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/.vs/skinet/DesignTimeBuild/.dtbcache.v2 -------------------------------------------------------------------------------- /.vs/skinet/v16/.suo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/.vs/skinet/v16/.suo -------------------------------------------------------------------------------- /.vs/skinet/v16/TestStore/0/000.testlog: -------------------------------------------------------------------------------- 1 | !!tItseT -------------------------------------------------------------------------------- /.vs/skinet/v16/TestStore/0/testlog.manifest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/.vs/skinet/v16/TestStore/0/testlog.manifest -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/API/bin/Debug/netcoreapp3.1/API.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/API", 16 | "stopAtEntry": false, 17 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser 18 | "serverReadyAction": { 19 | "action": "openExternally", 20 | "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" 21 | }, 22 | "env": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "sourceFileMap": { 26 | "/Views": "${workspaceFolder}/Views" 27 | } 28 | }, 29 | { 30 | "name": ".NET Core Attach", 31 | "type": "coreclr", 32 | "request": "attach", 33 | "processId": "${command:pickProcess}" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/API/API.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/API/API.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/API/API.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /API/API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | netcoreapp3.1 18 | 19 | -------------------------------------------------------------------------------- /API/API.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | ProjectDebugger 5 | 6 | 7 | API 8 | ApiControllerEmptyScaffolder 9 | root/Controller 10 | 600 11 | True 12 | False 13 | True 14 | 15 | False 16 | 17 | -------------------------------------------------------------------------------- /API/Content/images/products/boot-ang1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/boot-ang1.png -------------------------------------------------------------------------------- /API/Content/images/products/boot-ang2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/boot-ang2.png -------------------------------------------------------------------------------- /API/Content/images/products/boot-core1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/boot-core1.png -------------------------------------------------------------------------------- /API/Content/images/products/boot-core2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/boot-core2.png -------------------------------------------------------------------------------- /API/Content/images/products/boot-redis1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/boot-redis1.png -------------------------------------------------------------------------------- /API/Content/images/products/glove-code1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/glove-code1.png -------------------------------------------------------------------------------- /API/Content/images/products/glove-code2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/glove-code2.png -------------------------------------------------------------------------------- /API/Content/images/products/glove-react1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/glove-react1.png -------------------------------------------------------------------------------- /API/Content/images/products/glove-react2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/glove-react2.png -------------------------------------------------------------------------------- /API/Content/images/products/hat-core1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/hat-core1.png -------------------------------------------------------------------------------- /API/Content/images/products/hat-react1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/hat-react1.png -------------------------------------------------------------------------------- /API/Content/images/products/hat-react2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/hat-react2.png -------------------------------------------------------------------------------- /API/Content/images/products/sb-ang1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/sb-ang1.png -------------------------------------------------------------------------------- /API/Content/images/products/sb-ang2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/sb-ang2.png -------------------------------------------------------------------------------- /API/Content/images/products/sb-core1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/sb-core1.png -------------------------------------------------------------------------------- /API/Content/images/products/sb-core2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/sb-core2.png -------------------------------------------------------------------------------- /API/Content/images/products/sb-react1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/sb-react1.png -------------------------------------------------------------------------------- /API/Content/images/products/sb-ts1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobinHaider/An-E-commerce-app-with-.Net-Core-and-Angular/3f4ef5cae6b426b9cf0411324bc6674ec7e06551/API/Content/images/products/sb-ts1.png -------------------------------------------------------------------------------- /API/Controllers/BaseApiController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace API.Controllers 9 | { 10 | [Route("api/[controller]")] 11 | [ApiController] 12 | public class BaseApiController : ControllerBase 13 | { 14 | } 15 | } -------------------------------------------------------------------------------- /API/Controllers/BasketController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using API.Dtos; 6 | using AutoMapper; 7 | using Core.Entities; 8 | using Core.Interfaces; 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.AspNetCore.Mvc; 11 | 12 | namespace API.Controllers 13 | { 14 | 15 | public class BasketController : BaseApiController 16 | { 17 | private readonly IBasketRepository _basketRepository; 18 | private readonly IMapper _mapper; 19 | 20 | public BasketController(IBasketRepository basketRepository, IMapper mapper) 21 | { 22 | _mapper = mapper; 23 | _basketRepository = basketRepository; 24 | } 25 | 26 | [HttpGet] 27 | public async Task> GetBasketById(string id) 28 | { 29 | var basket = await _basketRepository.GetBasketAsync(id); 30 | 31 | return Ok(basket ?? new CustomerBasket(id)); 32 | } 33 | 34 | [HttpPost] 35 | public async Task> UpdateBasket(CustomerBasketDto basket) 36 | { 37 | var customerBasket = _mapper.Map(basket); 38 | 39 | var updatedBasket = await _basketRepository.UpdateBasketAsync(customerBasket); 40 | 41 | return Ok(updatedBasket); 42 | } 43 | 44 | [HttpDelete] 45 | public async Task DeleteBasketAsync(string id) 46 | { 47 | await _basketRepository.DeleteBasketAsync(id); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /API/Controllers/BuggyController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using API.Errors; 6 | using Infrastructure.Data; 7 | using Microsoft.AspNetCore.Authorization; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | namespace API.Controllers 12 | { 13 | [Route("api/[controller]")] 14 | [ApiController] 15 | public class BuggyController : ControllerBase 16 | { 17 | private readonly StoreContext _context; 18 | 19 | public BuggyController(StoreContext context) 20 | { 21 | _context = context; 22 | } 23 | 24 | [HttpGet("testauth")] 25 | [Authorize] 26 | public ActionResult GetSecretTest(){ 27 | return "secret text"; 28 | } 29 | 30 | [HttpGet("notfound")] 31 | public ActionResult GetNotFoundRequest() 32 | { 33 | var thing = _context.Products.Find(42); 34 | if (thing == null) 35 | { 36 | return NotFound(new ApiResponse(404)); 37 | } 38 | return Ok(); 39 | } 40 | 41 | [HttpGet("servererror")] 42 | public ActionResult GetServerError() 43 | { 44 | var thing = _context.Products.Find(42); 45 | var thingsToReturn = thing.ToString(); 46 | return Ok(); 47 | } 48 | 49 | [HttpGet("badrequest")] 50 | public ActionResult GetBadRequest() 51 | { 52 | return BadRequest(new ApiResponse(400)); 53 | } 54 | 55 | [HttpGet("badrequest/{id}")] 56 | public ActionResult GetNotFoundRequest(int id) 57 | { 58 | return Ok(); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /API/Controllers/ErrorController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using API.Errors; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | namespace API.Controllers 10 | { 11 | [Route("errors/{code}")] 12 | [ApiExplorerSettings(IgnoreApi = true)] 13 | public class ErrorController : BaseApiController 14 | { 15 | public IActionResult Error(int code) 16 | { 17 | return new ObjectResult(new ApiResponse(code)); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /API/Controllers/FallbackController.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace API.Controllers 5 | { 6 | public class FallbackController : Controller 7 | { 8 | public IActionResult Index() 9 | { 10 | return PhysicalFile(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html"), "text/HTML"); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /API/Controllers/OrdersController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Security.Claims; 4 | using System.Threading.Tasks; 5 | using API.Dtos; 6 | using API.Errors; 7 | using API.Extensions; 8 | using AutoMapper; 9 | using Core.Entities.OrderAggregate; 10 | using Core.Interfaces; 11 | using Microsoft.AspNetCore.Authorization; 12 | using Microsoft.AspNetCore.Mvc; 13 | 14 | namespace API.Controllers 15 | { 16 | [Authorize] 17 | public class OrdersController : BaseApiController 18 | { 19 | private readonly IOrderService _orderService; 20 | private readonly IMapper _mapper; 21 | public OrdersController(IOrderService orderService, IMapper mapper) 22 | { 23 | _mapper = mapper; 24 | _orderService = orderService; 25 | } 26 | 27 | [HttpPost] 28 | public async Task> CreateOrder(OrderDto orderDto) 29 | { 30 | var email = HttpContext.User.RetriveEmailFromPrincipal(); 31 | 32 | var address = _mapper.Map(orderDto.ShipToAddress); 33 | 34 | var order = await _orderService.CreateOrderAsync(email, orderDto.DeliveryMethodId, orderDto.BasketId, address); 35 | 36 | if (order == null) return BadRequest(new ApiResponse(400, "Problem creating order")); 37 | 38 | return Ok(order); 39 | } 40 | 41 | [HttpGet] 42 | public async Task>> GetOrderForUser() 43 | { 44 | var email = HttpContext.User.RetriveEmailFromPrincipal(); 45 | 46 | var orders = await _orderService.GetOrdersForUserAsync(email); 47 | 48 | return Ok(_mapper.Map, IReadOnlyList>(orders)); 49 | } 50 | 51 | [HttpGet("{id}")] 52 | public async Task> GetOrderByIdForUser(int id) 53 | { 54 | var email = HttpContext.User.RetriveEmailFromPrincipal(); 55 | 56 | var order = await _orderService.GetOrderByIdAsync(id, email); 57 | 58 | if(order == null) return NotFound(new ApiResponse(404)); 59 | 60 | return _mapper.Map(order); 61 | } 62 | 63 | [HttpGet("deliveryMethods")] 64 | public async Task>> GetDeliveryMethods() 65 | { 66 | return Ok(await _orderService.GetDeliveryMethodAsync()); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /API/Dtos/AddressDto.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace API.Dtos 4 | { 5 | public class AddressDto 6 | { 7 | [Required] 8 | public string FirstName { get; set; } 9 | [Required] 10 | public string LastName { get; set; } 11 | [Required] 12 | public string Street { get; set; } 13 | [Required] 14 | public string City { get; set; } 15 | [Required] 16 | public string State { get; set; } 17 | [Required] 18 | public string Zipcode { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /API/Dtos/BasketItemDto.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace API.Dtos 4 | { 5 | public class BasketItemDto 6 | { 7 | [Required] 8 | public int Id { get; set; } 9 | [Required] 10 | public string ProductName { get; set; } 11 | [Required] 12 | [Range(0.1, double.MaxValue, ErrorMessage="Price must be greater than zero")] 13 | public decimal Price { get; set; } 14 | [Required] 15 | [Range(1, double.MaxValue, ErrorMessage="Quantity must be at least one")] 16 | public int Quantity { get; set; } 17 | [Required] 18 | public string PictureUrl { get; set; } 19 | [Required] 20 | public string Brand { get; set; } 21 | [Required] 22 | public string Type { get; set; } 23 | } 24 | } -------------------------------------------------------------------------------- /API/Dtos/CustomerBasketDto.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace API.Dtos 5 | { 6 | public class CustomerBasketDto 7 | { 8 | [Required] 9 | public string Id { get; set; } 10 | public List Items { get; set; } 11 | public int? DeliveryMethodId { get; set; } 12 | public string ClientSecret { get; set; } 13 | public string PaymentIntentId { get; set; } 14 | public decimal ShippingPrice { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /API/Dtos/LoginDto.cs: -------------------------------------------------------------------------------- 1 | namespace API.Dtos 2 | { 3 | public class LoginDto 4 | { 5 | public string Email { get; set; } 6 | public string Password { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /API/Dtos/OrderDto.cs: -------------------------------------------------------------------------------- 1 | namespace API.Dtos 2 | { 3 | public class OrderDto 4 | { 5 | public string BasketId { get; set; } 6 | public int DeliveryMethodId { get; set; } 7 | public AddressDto ShipToAddress { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /API/Dtos/OrderItemDto.cs: -------------------------------------------------------------------------------- 1 | namespace API.Dtos 2 | { 3 | public class OrderItemDto 4 | { 5 | public int ProductId { get; set; } 6 | public string ProductName { get; set; } 7 | public string PictureUrl { get; set; } 8 | public decimal Price { get; set; } 9 | public int Quantity { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /API/Dtos/OrderToReturnDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Core.Entities.OrderAggregate; 4 | 5 | namespace API.Dtos 6 | { 7 | public class OrderToReturnDto 8 | { 9 | public int Id { get; set; } 10 | public string BuyerEmail { get; set; } 11 | public DateTimeOffset OrderDate { get; set; } 12 | public Address ShipToAddress { get; set; } 13 | public string DeliveryMethod { get; set; } 14 | public decimal ShippingPrice { get; set; } 15 | public IReadOnlyList OrderItems { get; set; } 16 | public decimal Subtotal { get; set; } 17 | public decimal Total { get; set; } 18 | public string Status { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /API/Dtos/ProductToReturnDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Core.Entities; 6 | 7 | namespace API.Dtos 8 | { 9 | public class ProductToReturnDto 10 | { 11 | public int Id { get; set; } 12 | public string Name { get; set; } 13 | public string Description { get; set; } 14 | public decimal Price { get; set; } 15 | public string PictureUrl { get; set; } 16 | public string ProductType { get; set; } 17 | public string ProductBrand { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /API/Dtos/RegisterDto.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace API.Dtos 4 | { 5 | public class RegisterDto 6 | { 7 | [Required] 8 | public string DisplayName { get; set; } 9 | [Required] 10 | [EmailAddress] 11 | public string Email { get; set; } 12 | [Required] 13 | [RegularExpression("(?=^.{6,10}$)(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+}{":;'?/>.<,])(?!.*\\s).*$", ErrorMessage = "password must have at least 1 small-case letter, 1 Capital letter, 1 digit, 1 special character and the length should be between 6-10 characters")] 14 | public string Password { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /API/Dtos/UserDto.cs: -------------------------------------------------------------------------------- 1 | namespace API.Dtos 2 | { 3 | public class UserDto 4 | { 5 | public string Email { get; set; } 6 | public string DisplayName { get; set; } 7 | public string Token { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /API/Errors/ApiException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace API.Errors 7 | { 8 | public class ApiException : ApiResponse 9 | { 10 | public ApiException(int statusCode, string message = null, string details = null) : base(statusCode, message) 11 | { 12 | Details = details; 13 | } 14 | 15 | public string Details { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /API/Errors/ApiResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace API.Errors 8 | { 9 | public class ApiResponse 10 | { 11 | public ApiResponse(int statusCode, string message = null) 12 | { 13 | StatusCode = statusCode; 14 | Message = message ?? GetDefaultMessageForStatusCode(statusCode); 15 | } 16 | 17 | 18 | public int StatusCode { get; set; } 19 | public string Message { get; set; } 20 | 21 | private string GetDefaultMessageForStatusCode(in int statusCode) 22 | { 23 | return statusCode switch 24 | { 25 | 400=>"A bad request, you have made", 26 | 401=> "Authorized, you are not", 27 | 404=> "Resource found, you are not", 28 | 500=> 29 | "Errors are the path to the dark side, Errors lead to the anger. Anger lead to hate. Hate leads to career change." 30 | , 31 | _ => null 32 | }; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /API/Errors/ApiValidationErrorResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace API.Errors 7 | { 8 | public class ApiValidationErrorResponse : ApiResponse 9 | { 10 | public ApiValidationErrorResponse() : base(400) 11 | { 12 | 13 | } 14 | 15 | public IEnumerable Errors { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /API/Extensions/ApplicationServicesExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using API.Errors; 3 | using Core.Interfaces; 4 | using Infrastructure.Data; 5 | using Infrastructure.Services; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | namespace API.Extensions 10 | { 11 | public static class ApplicationServicesExtensions 12 | { 13 | public static IServiceCollection AddApplicationServices(this IServiceCollection services) 14 | { 15 | services.AddSingleton(); 16 | 17 | services.AddScoped(); 18 | 19 | services.AddScoped(); 20 | 21 | services.AddScoped(); 22 | 23 | services.AddScoped(); 24 | 25 | services.AddScoped(); 26 | 27 | services.AddScoped(); 28 | 29 | services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>)); 30 | 31 | services.Configure(options => 32 | { 33 | options.InvalidModelStateResponseFactory = actionContext => 34 | { 35 | var errors = actionContext.ModelState 36 | .Where(e => e.Value.Errors.Count > 0) 37 | .SelectMany(x => x.Value.Errors) 38 | .Select(x => x.ErrorMessage).ToArray(); 39 | 40 | var errorResponse = new ApiValidationErrorResponse 41 | { 42 | Errors = errors 43 | }; 44 | 45 | return new BadRequestObjectResult(errorResponse); 46 | }; 47 | }); 48 | 49 | return services; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /API/Extensions/ClaimsPrincipalExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Security.Claims; 3 | 4 | namespace API.Extensions 5 | { 6 | public static class ClaimsPrincipalExtension 7 | { 8 | public static string RetriveEmailFromPrincipal(this ClaimsPrincipal user) 9 | { 10 | return user?.Claims?.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value; 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /API/Extensions/IdentityServiceExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Core.Entities.Identity; 7 | using Infrastructure.Identity; 8 | using Microsoft.AspNetCore.Authentication.JwtBearer; 9 | using Microsoft.AspNetCore.Identity; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.IdentityModel.Tokens; 13 | 14 | namespace API.Extensions 15 | { 16 | public static class IdentityServiceExtension 17 | { 18 | public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration config) 19 | { 20 | var builder = services.AddIdentityCore(); 21 | 22 | builder = new IdentityBuilder(builder.UserType, builder.Services); 23 | builder.AddEntityFrameworkStores(); 24 | builder.AddSignInManager>(); 25 | 26 | services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 27 | .AddJwtBearer(options => { 28 | options.TokenValidationParameters = new TokenValidationParameters{ 29 | ValidateIssuerSigningKey = true, 30 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Token:Key"])), 31 | ValidIssuer = config["Token:Issuer"], 32 | ValidateIssuer = true, 33 | ValidateAudience = false 34 | }; 35 | }); 36 | 37 | return services; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /API/Extensions/SwaggerServiceExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.OpenApi.Models; 4 | 5 | namespace API.Extensions 6 | { 7 | public static class SwaggerServiceExtension 8 | { 9 | public static IServiceCollection AddSwaggerDocumentation(this IServiceCollection services) 10 | { 11 | services.AddSwaggerGen(c => 12 | { 13 | c.SwaggerDoc("v1", new OpenApiInfo {Title = "SkiNet API", Version = "v1"}); 14 | 15 | var securitySchema = new OpenApiSecurityScheme{ 16 | Description = "JWT Auth Bearer Scheme", 17 | Name = "Authorization", 18 | In = ParameterLocation.Header, 19 | Type = SecuritySchemeType.Http, 20 | Scheme = "bearer", 21 | Reference = new OpenApiReference { 22 | Type = ReferenceType.SecurityScheme, 23 | Id = "Bearer" 24 | } 25 | }; 26 | 27 | c.AddSecurityDefinition("Bearer", securitySchema); 28 | var securityRequirement = new OpenApiSecurityRequirement{{securitySchema, new[] {"Bearer"}}}; 29 | c.AddSecurityRequirement(securityRequirement); 30 | }); 31 | 32 | return services; 33 | } 34 | 35 | public static IApplicationBuilder UseSwaggerDocumentaion(this IApplicationBuilder app) 36 | { 37 | app.UseSwagger(); 38 | app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "SkiNet API v1"); }); 39 | 40 | return app; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /API/Extensions/UserManagerExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Security.Claims; 3 | using System.Threading.Tasks; 4 | using Core.Entities.Identity; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | namespace API.Extensions 9 | { 10 | public static class UserManagerExtension 11 | { 12 | public static async Task FindByEmailWithAddressAsync(this UserManager input, ClaimsPrincipal user){ 13 | var email = user?.Claims?.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value; 14 | 15 | return await input.Users.Include(x => x.Address).SingleOrDefaultAsync(x => x.Email == email); 16 | } 17 | 18 | public static async Task FindByEmailFromClaimsPrincipal(this UserManager input, ClaimsPrincipal user){ 19 | var email = user?.Claims?.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value; 20 | 21 | return await input.Users.SingleOrDefaultAsync(x => x.Email == email); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /API/Helpers/CachedAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Core.Interfaces; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.Filters; 9 | using Microsoft.Extensions.DependencyInjection; 10 | 11 | namespace API.Helpers 12 | { 13 | public class CachedAttribute : Attribute, IAsyncActionFilter 14 | { 15 | private readonly int _timeToLiveSeconds; 16 | public CachedAttribute(int timeToLiveSeconds) 17 | { 18 | _timeToLiveSeconds = timeToLiveSeconds; 19 | } 20 | 21 | public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) 22 | { 23 | var cacheService = context.HttpContext.RequestServices.GetRequiredService(); 24 | 25 | var cacheKey = GenerateCacheKeyFromRequest(context.HttpContext.Request); 26 | var cacheResponse = await cacheService.GetCacheResponseAsync(cacheKey); 27 | 28 | if (!string.IsNullOrEmpty(cacheResponse)) 29 | { 30 | var contentResult = new ContentResult 31 | { 32 | Content = cacheResponse, 33 | ContentType = "application/json", 34 | StatusCode = 200 35 | }; 36 | 37 | context.Result = contentResult; 38 | 39 | return; 40 | } 41 | 42 | //If it not found in cache then execute this.. 43 | var executedContext = await next(); //move to controller 44 | if(executedContext.Result is OkObjectResult okObjectResult) 45 | { 46 | await cacheService.CacheResponseAsync(cacheKey, okObjectResult.Value, TimeSpan.FromSeconds(_timeToLiveSeconds)); 47 | } 48 | } 49 | 50 | private string GenerateCacheKeyFromRequest(HttpRequest request) 51 | { 52 | var keyBuilder = new StringBuilder(); 53 | 54 | keyBuilder.Append($"{request.Path}"); 55 | 56 | foreach (var (key, value) in request.Query.OrderBy(o=>o.Key)) 57 | { 58 | keyBuilder.Append($"|{key}-{value}"); 59 | } 60 | 61 | return keyBuilder.ToString(); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /API/Helpers/MappingProfiles.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using API.Dtos; 6 | using AutoMapper; 7 | using Core.Entities; 8 | using Core.Entities.Identity; 9 | using Core.Entities.OrderAggregate; 10 | 11 | namespace API.Helpers 12 | { 13 | public class MappingProfiles : Profile 14 | { 15 | public MappingProfiles() 16 | { 17 | CreateMap() 18 | .ForMember(d => d.ProductBrand, o => o.MapFrom(s => s.ProductBrand.Name)) 19 | .ForMember(d => d.ProductType, o => o.MapFrom(s => s.ProductType.Name)) 20 | .ForMember(d => d.PictureUrl, o => o.MapFrom()); 21 | 22 | CreateMap().ReverseMap(); 23 | CreateMap(); 24 | CreateMap(); 25 | CreateMap(); 26 | CreateMap() 27 | .ForMember(d => d.DeliveryMethod, o=>o.MapFrom(s=>s.DeliveryMethod.ShortName)) 28 | .ForMember(d => d.ShippingPrice, o=>o.MapFrom(s=>s.DeliveryMethod.Price)); 29 | CreateMap() 30 | .ForMember(d=>d.ProductId, o=>o.MapFrom(s=>s.ItemOrdered.ProductItemId)) 31 | .ForMember(d=>d.ProductName, o=>o.MapFrom(s=>s.ItemOrdered.ProductName)) 32 | .ForMember(d=>d.PictureUrl, o=>o.MapFrom(s=>s.ItemOrdered.PictureUrl)) 33 | .ForMember(d=>d.PictureUrl, o=>o.MapFrom()); 34 | } 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /API/Helpers/OrderItemUrlResolver.cs: -------------------------------------------------------------------------------- 1 | using API.Dtos; 2 | using AutoMapper; 3 | using Core.Entities.OrderAggregate; 4 | using Microsoft.Extensions.Configuration; 5 | 6 | namespace API.Helpers 7 | { 8 | public class OrderItemUrlResolver : IValueResolver 9 | { 10 | private readonly IConfiguration _config; 11 | public OrderItemUrlResolver(IConfiguration config) 12 | { 13 | _config = config; 14 | } 15 | 16 | public string Resolve(OrderItem source, OrderItemDto destination, string destMember, ResolutionContext context) 17 | { 18 | if (!string.IsNullOrEmpty(source.ItemOrdered.PictureUrl)) 19 | { 20 | return _config["ApiUrl"] + source.ItemOrdered.PictureUrl; 21 | } 22 | 23 | return null; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /API/Helpers/Pagination.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace API.Helpers 7 | { 8 | public class Pagination where T: class 9 | { 10 | public int PageIndex { get; set; } 11 | public int PageSize { get; set; } 12 | public int Count { get; set; } 13 | public IReadOnlyList Data { get; set; } 14 | 15 | public Pagination(int pageIndex, int pageSize, int count, IReadOnlyList data) 16 | { 17 | PageIndex = pageIndex; 18 | PageSize = pageSize; 19 | Count = count; 20 | Data = data; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /API/Helpers/ProductUrlResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using API.Dtos; 6 | using AutoMapper; 7 | using Core.Entities; 8 | using Microsoft.Extensions.Configuration; 9 | 10 | namespace API.Helpers 11 | { 12 | public class ProductUrlResolver : IValueResolver 13 | { 14 | private readonly IConfiguration _config; 15 | 16 | public ProductUrlResolver(IConfiguration config) 17 | { 18 | _config = config; 19 | } 20 | 21 | public string Resolve(Product source, ProductToReturnDto destination, string destMember, 22 | ResolutionContext context) 23 | { 24 | if (!string.IsNullOrEmpty(source.PictureUrl)) 25 | { 26 | return _config["ApiUrl"] + source.PictureUrl; 27 | } 28 | 29 | return null; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /API/Middleware/ExceptionMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | using API.Errors; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace API.Middleware 13 | { 14 | public class ExceptionMiddleware 15 | { 16 | private readonly RequestDelegate _next; 17 | private readonly ILogger _logger; 18 | private readonly IHostEnvironment _env; 19 | 20 | public ExceptionMiddleware(RequestDelegate next, ILogger logger, 21 | IHostEnvironment env 22 | ) 23 | { 24 | _next = next; 25 | _logger = logger; 26 | _env = env; 27 | } 28 | 29 | public async Task InvokeAsync(HttpContext context) 30 | { 31 | try 32 | { 33 | await _next(context); 34 | } 35 | catch (Exception ex) 36 | { 37 | _logger.LogError(ex, ex.Message); 38 | context.Response.ContentType = "application/json"; 39 | context.Response.StatusCode = (int) HttpStatusCode.InternalServerError; 40 | 41 | var response = _env.IsDevelopment() 42 | ? new ApiException((int) HttpStatusCode.InternalServerError, ex.Message, ex.StackTrace.ToString()) 43 | : new ApiException((int) HttpStatusCode.InternalServerError); 44 | 45 | var options = new JsonSerializerOptions{PropertyNamingPolicy = JsonNamingPolicy.CamelCase}; 46 | 47 | var json = JsonSerializer.Serialize(response, options); 48 | 49 | await context.Response.WriteAsync(json); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /API/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Core.Entities.Identity; 4 | using Infrastructure.Data; 5 | using Infrastructure.Identity; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Identity; 8 | using Microsoft.EntityFrameworkCore; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace API 14 | { 15 | public class Program 16 | { 17 | public static async Task Main(string[] args) 18 | { 19 | var host = CreateHostBuilder(args).Build(); 20 | using (var scope = host.Services.CreateScope()) 21 | { 22 | var services = scope.ServiceProvider; 23 | var loggerFactory = services.GetRequiredService(); 24 | try 25 | { 26 | var context = services.GetRequiredService(); 27 | await context.Database.MigrateAsync(); 28 | await StoreContextSeed.SeedAsync(context, loggerFactory); 29 | 30 | var userManager = services.GetRequiredService>(); 31 | var identityContext = services.GetRequiredService(); 32 | await identityContext.Database.MigrateAsync(); 33 | await AppIdentityDbContextSeed.SeedUserAsync(userManager); 34 | } 35 | catch (Exception ex) 36 | { 37 | var logger = loggerFactory.CreateLogger(); 38 | logger.LogError(ex, "An error occured during migration"); 39 | } 40 | } 41 | 42 | host.Run(); 43 | } 44 | 45 | public static IHostBuilder CreateHostBuilder(string[] args) => 46 | Host.CreateDefaultBuilder(args) 47 | .ConfigureWebHostDefaults(webBuilder => 48 | { 49 | webBuilder.UseStartup(); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:43280", 8 | "sslPort": 44376 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": false, 15 | "launchUrl": "weatherforecast", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "API": { 21 | "commandName": "Project", 22 | "launchBrowser": false, 23 | "launchUrl": "weatherforecast", 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Information", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "ConnectionStrings": { 10 | "DefaultConnection": "Data source=skinet.db", 11 | "IdentityConnection": "Data source=identity.db", 12 | "Redis": "localhost" 13 | }, 14 | "Token": { 15 | "Key": "super secret key", 16 | "Issuer": "https://localhost:5001" 17 | }, 18 | "ApiUrl": "https://localhost:5001/Content/" 19 | } 20 | -------------------------------------------------------------------------------- /API/wwwroot/8-es2015.ecc3cf4fe5764b865c78.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[8],{SCLQ:function(e,t,n){"use strict";n.r(t),n.d(t,"BasketModule",(function(){return f}));var c=n("PCNd"),r=n("ofXK"),i=n("fXoL"),o=n("cAP4"),s=n("GJcC"),b=n("PoZw"),a=n("tyNb");function m(e,t){1&e&&(i.Sb(0,"div"),i.Sb(1,"p"),i.Ac(2,"There are no items in your basket"),i.Rb(),i.Rb())}function u(e,t){if(1&e){const e=i.Tb();i.Sb(0,"div"),i.Sb(1,"div",2),i.Sb(2,"div",3),i.Sb(3,"div",4),i.Sb(4,"div",5),i.Sb(5,"app-basket-summary",6),i.ac("decrement",(function(t){return i.sc(e),i.cc().decrementItemQuantity(t)}))("increment",(function(t){return i.sc(e),i.cc().incrementItemQuantity(t)}))("remove",(function(t){return i.sc(e),i.cc().removeBasketItem(t)})),i.Rb(),i.Rb(),i.Rb(),i.Sb(6,"div",4),i.Sb(7,"div",7),i.Nb(8,"app-order-totals"),i.Sb(9,"a",8),i.Ac(10," Proceed to checkout "),i.Rb(),i.Rb(),i.Rb(),i.Rb(),i.Rb(),i.Rb()}}const d=[{path:"",component:(()=>{class e{constructor(e){this.basketService=e}ngOnInit(){this.basket$=this.basketService.basket$}removeBasketItem(e){this.basketService.removeItemFromBasket(e)}incrementItemQuantity(e){this.basketService.incrementItemQuantity(e)}decrementItemQuantity(e){this.basketService.decrementItemQuantity(e)}}return e.\u0275fac=function(t){return new(t||e)(i.Mb(o.a))},e.\u0275cmp=i.Gb({type:e,selectors:[["app-basket"]],decls:5,vars:6,consts:[[1,"container","mt-2"],[4,"ngIf"],[1,"pb-5"],[1,"container"],[1,"row"],[1,"col-12","py-5","mb-1"],[3,"decrement","increment","remove"],[1,"col-6","offset-6"],["routerLink","/checkout",1,"btn","btn-outline-primary","py-2","btn-block"]],template:function(e,t){1&e&&(i.Sb(0,"div",0),i.yc(1,m,3,0,"div",1),i.dc(2,"async"),i.yc(3,u,11,0,"div",1),i.dc(4,"async"),i.Rb()),2&e&&(i.Bb(1),i.ic("ngIf",null===i.ec(2,2,t.basket$)),i.Bb(2),i.ic("ngIf",i.ec(4,4,t.basket$)))},directives:[r.m,s.a,b.a,a.f],pipes:[r.b],styles:[""]}),e})()}];let p=(()=>{class e{}return e.\u0275mod=i.Kb({type:e}),e.\u0275inj=i.Jb({factory:function(t){return new(t||e)},imports:[[a.g.forChild(d)],a.g]}),e})(),f=(()=>{class e{}return e.\u0275mod=i.Kb({type:e}),e.\u0275inj=i.Jb({factory:function(t){return new(t||e)},imports:[[r.c,p,c.a]]}),e})()}}]); -------------------------------------------------------------------------------- /API/wwwroot/8-es5.ecc3cf4fe5764b865c78.js: -------------------------------------------------------------------------------- 1 | function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _defineProperties(e,t){for(var n=0;n 2 | 3 | 4 | 5 | SkiNet 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /API/wwwroot/runtime-es2015.597c9a66078d39006feb.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,c,a=r[0],i=r[1],f=r[2],p=0,s=[];p 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Core/Entities/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Core.Entities 2 | { 3 | public class BaseEntity 4 | { 5 | public int Id { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /Core/Entities/BasketItem.cs: -------------------------------------------------------------------------------- 1 | namespace Core.Entities 2 | { 3 | public class BasketItem 4 | { 5 | public int Id { get; set; } 6 | public string ProductName { get; set; } 7 | public decimal Price { get; set; } 8 | public int Quantity { get; set; } 9 | public string PictureUrl { get; set; } 10 | public string Brand { get; set; } 11 | public string Type { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /Core/Entities/CustomerBasket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Core.Entities 6 | { 7 | public class CustomerBasket 8 | { 9 | public CustomerBasket() 10 | { 11 | 12 | } 13 | public CustomerBasket(string id) 14 | { 15 | Id = id; 16 | } 17 | public string Id { get; set; } 18 | public List Items { get; set; } = new List(); 19 | public int? DeliveryMethodId { get; set; } 20 | public string ClientSecret { get; set; } 21 | public string PaymentIntentId { get; set; } 22 | public decimal ShippingPrice { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Core/Entities/Identity/Address.cs: -------------------------------------------------------------------------------- 1 | namespace Core.Entities.Identity 2 | { 3 | public class Address 4 | { 5 | public int Id { get; set; } 6 | public string FirstName { get; set; } 7 | public string LastName { get; set; } 8 | public string Street { get; set; } 9 | public string City { get; set; } 10 | public string State { get; set; } 11 | public string Zipcode { get; set; } 12 | public string AppUserId { get; set; } 13 | public AppUser AppUser { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /Core/Entities/Identity/AppUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Microsoft.AspNetCore.Identity; 5 | 6 | namespace Core.Entities.Identity 7 | { 8 | public class AppUser : IdentityUser 9 | { 10 | public string DisplayName { get; set; } 11 | public Address Address { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Core/Entities/OrderAggregate/Address.cs: -------------------------------------------------------------------------------- 1 | namespace Core.Entities.OrderAggregate 2 | { 3 | public class Address 4 | { 5 | public Address() 6 | { 7 | } 8 | 9 | public Address(string firstName, string lastName, string street, string city, string state, string zipcode) 10 | { 11 | FirstName = firstName; 12 | LastName = lastName; 13 | Street = street; 14 | City = city; 15 | State = state; 16 | Zipcode = zipcode; 17 | } 18 | 19 | public string FirstName { get; set; } 20 | public string LastName { get; set; } 21 | public string Street { get; set; } 22 | public string City { get; set; } 23 | public string State { get; set; } 24 | public string Zipcode { get; set; } 25 | } 26 | } -------------------------------------------------------------------------------- /Core/Entities/OrderAggregate/DeliveryMethod.cs: -------------------------------------------------------------------------------- 1 | namespace Core.Entities.OrderAggregate 2 | { 3 | public class DeliveryMethod : BaseEntity 4 | { 5 | public string ShortName { get; set; } 6 | public string DeliveryTime { get; set; } 7 | public string Description { get; set; } 8 | public decimal Price { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /Core/Entities/OrderAggregate/Order.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Core.Entities.OrderAggregate 5 | { 6 | public class Order : BaseEntity 7 | { 8 | public Order() 9 | { 10 | 11 | } 12 | 13 | public Order(IReadOnlyList orderItems, string buyerEmail, Address shipToAddress, DeliveryMethod deliveryMethod, decimal subtotal, string paymentIntentId) 14 | { 15 | BuyerEmail = buyerEmail; 16 | ShipToAddress = shipToAddress; 17 | DeliveryMethod = deliveryMethod; 18 | OrderItems = orderItems; 19 | Subtotal = subtotal; 20 | PaymentIntentId = paymentIntentId; 21 | } 22 | 23 | public string BuyerEmail { get; set; } 24 | public DateTimeOffset OrderDate { get; set; } = DateTimeOffset.Now; 25 | public Address ShipToAddress { get; set; } 26 | public DeliveryMethod DeliveryMethod { get; set; } 27 | public IReadOnlyList OrderItems { get; set; } 28 | public decimal Subtotal { get; set; } 29 | public OrderStatus Status { get; set; } = OrderStatus.Pending; 30 | public string PaymentIntentId { get; set; } 31 | 32 | public decimal GetTotal(){ 33 | return Subtotal + DeliveryMethod.Price; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /Core/Entities/OrderAggregate/OrderItem.cs: -------------------------------------------------------------------------------- 1 | namespace Core.Entities.OrderAggregate 2 | { 3 | public class OrderItem : BaseEntity 4 | { 5 | public OrderItem() 6 | { 7 | } 8 | 9 | public OrderItem(ProductItemOrdered itemOrdered, decimal price, int quantity) 10 | { 11 | ItemOrdered = itemOrdered; 12 | Price = price; 13 | Quantity = quantity; 14 | } 15 | 16 | public ProductItemOrdered ItemOrdered { get; set; } 17 | public decimal Price { get; set; } 18 | public int Quantity { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /Core/Entities/OrderAggregate/OrderStatus.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace Core.Entities.OrderAggregate 4 | { 5 | public enum OrderStatus 6 | { 7 | [EnumMember(Value = "Pending")] 8 | Pending, 9 | [EnumMember(Value = "Payment Received")] 10 | PaymentReceived, 11 | [EnumMember(Value = "Payment Failed")] 12 | PaymentFailed 13 | } 14 | } -------------------------------------------------------------------------------- /Core/Entities/OrderAggregate/ProductItemOrdered.cs: -------------------------------------------------------------------------------- 1 | namespace Core.Entities.OrderAggregate 2 | { 3 | public class ProductItemOrdered 4 | { 5 | public ProductItemOrdered() 6 | { 7 | } 8 | 9 | public ProductItemOrdered(int productItemId, string productName, string pictureUrl) 10 | { 11 | ProductItemId = productItemId; 12 | ProductName = productName; 13 | PictureUrl = pictureUrl; 14 | } 15 | 16 | public int ProductItemId { get; set; } 17 | public string ProductName { get; set; } 18 | public string PictureUrl { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /Core/Entities/Product.cs: -------------------------------------------------------------------------------- 1 | namespace Core.Entities 2 | { 3 | public class Product : BaseEntity 4 | { 5 | public string Name { get; set; } 6 | public string Description { get; set; } 7 | public decimal Price { get; set; } 8 | public string PictureUrl { get; set; } 9 | public ProductType ProductType { get; set; } 10 | public int ProductTypeId { get; set; } 11 | public ProductBrand ProductBrand { get; set; } 12 | public int ProductBrandId { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /Core/Entities/ProductBrand.cs: -------------------------------------------------------------------------------- 1 | namespace Core.Entities 2 | { 3 | public class ProductBrand : BaseEntity 4 | { 5 | public string Name { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /Core/Entities/ProductType.cs: -------------------------------------------------------------------------------- 1 | namespace Core.Entities 2 | { 3 | public class ProductType : BaseEntity 4 | { 5 | public string Name { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /Core/Interfaces/IBasketRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Core.Entities; 6 | 7 | namespace Core.Interfaces 8 | { 9 | public interface IBasketRepository 10 | { 11 | Task GetBasketAsync(string basketId); 12 | Task UpdateBasketAsync(CustomerBasket basket); 13 | Task DeleteBasketAsync(string id); 14 | 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Core/Interfaces/IGenericRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Core.Entities; 6 | using Core.Specification; 7 | 8 | namespace Core.Interfaces 9 | { 10 | public interface IGenericRepository where T: BaseEntity 11 | { 12 | Task GetByIdAsync(int id); 13 | Task> ListAllAsync(); 14 | Task GetEntityWithSpec(ISpecification spec); 15 | Task> ListAsync(ISpecification spec); 16 | Task CountAsync(ISpecification spec); 17 | void Add(T entity); 18 | void Update(T entity); 19 | void Delete(T entity); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Core/Interfaces/IOrderService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Core.Entities.OrderAggregate; 6 | 7 | namespace Core.Interfaces 8 | { 9 | public interface IOrderService 10 | { 11 | Task CreateOrderAsync(string buyerEmail, int deliveryMethod, string basketId, Address shippingAddress); 12 | Task> GetOrdersForUserAsync(string buyerEmail); 13 | Task GetOrderByIdAsync(int id, string buyerEmail); 14 | Task> GetDeliveryMethodAsync(); 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Core/Interfaces/IPaymentService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Core.Entities; 3 | using Core.Entities.OrderAggregate; 4 | 5 | namespace Core.Interfaces 6 | { 7 | public interface IPaymentService 8 | { 9 | Task CreateOrUpdatePaymentIntent(string basketId); 10 | Task UpdateOrderPaymentSucceeded(string paymentIntentId); 11 | Task UpdateOrderPaymentFailed(string paymentIntentId); 12 | } 13 | } -------------------------------------------------------------------------------- /Core/Interfaces/IProductRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Core.Entities; 4 | 5 | namespace Core.Interfaces 6 | { 7 | public interface IProductRepository 8 | { 9 | Task GetProductByIdAsync(int id); 10 | Task> GetProductsAsync(); 11 | Task> GetProductBrandsAsync(); 12 | Task> GetProductTypesAsync(); 13 | } 14 | } -------------------------------------------------------------------------------- /Core/Interfaces/IResponseCacheService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Core.Interfaces 5 | { 6 | public interface IResponseCacheService 7 | { 8 | Task CacheResponseAsync(string cacheKey, object response, TimeSpan timeToLive); 9 | Task GetCacheResponseAsync(string cacheKey); 10 | } 11 | } -------------------------------------------------------------------------------- /Core/Interfaces/ITokenService.cs: -------------------------------------------------------------------------------- 1 | using Core.Entities.Identity; 2 | 3 | namespace Core.Interfaces 4 | { 5 | public interface ITokenService 6 | { 7 | string CreateToken(AppUser user); 8 | } 9 | } -------------------------------------------------------------------------------- /Core/Interfaces/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Core.Entities; 4 | 5 | namespace Core.Interfaces 6 | { 7 | public interface IUnitOfWork : IDisposable 8 | { 9 | IGenericRepository Repository() where TEntity : BaseEntity; 10 | Task Complete(); 11 | 12 | } 13 | } -------------------------------------------------------------------------------- /Core/Specification/BaseSpecipication.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Text; 5 | 6 | namespace Core.Specification 7 | { 8 | public class BaseSpecipication : ISpecification 9 | { 10 | public BaseSpecipication() 11 | { 12 | 13 | } 14 | public BaseSpecipication(Expression> criteria) 15 | { 16 | Criteria = criteria; 17 | } 18 | 19 | public Expression> Criteria { get; } 20 | public List>> Includes { get; } 21 | = new List>>(); 22 | 23 | public Expression> OrderBy { get; private set; } 24 | public Expression> OrderByDescending { get; private set; } 25 | 26 | public int Take { get; private set; } 27 | public int Skip { get; private set; } 28 | public bool IsPagingEnabled { get; private set; } 29 | 30 | protected void AddInclude(Expression> includeExpression) 31 | { 32 | Includes.Add(includeExpression); 33 | } 34 | 35 | protected void AddOrderBy(Expression> orderByExpression) 36 | { 37 | OrderBy = orderByExpression; 38 | } 39 | 40 | protected void AddOrderByDescending(Expression> orderByDscExpression) 41 | { 42 | OrderByDescending = orderByDscExpression; 43 | } 44 | 45 | protected void ApplyPaging(int skip, int take) 46 | { 47 | Skip = skip; 48 | Take = take; 49 | IsPagingEnabled = true; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Core/Specification/ISpecification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Text; 5 | 6 | namespace Core.Specification 7 | { 8 | public interface ISpecification 9 | { 10 | Expression> Criteria { get; } 11 | List>> Includes { get; } 12 | Expression> OrderBy { get; } 13 | Expression> OrderByDescending { get; } 14 | int Take { get; } 15 | int Skip { get; } 16 | bool IsPagingEnabled { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Core/Specification/OrderByPaymentIntentWithItemSpecification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using Core.Entities.OrderAggregate; 4 | 5 | namespace Core.Specification 6 | { 7 | public class OrderByPaymentIntentWithItemSpecification : BaseSpecipication 8 | { 9 | public OrderByPaymentIntentWithItemSpecification(string paymentIntentId) : 10 | base(o => o.PaymentIntentId == paymentIntentId) 11 | { 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /Core/Specification/OrderWithItemsAndOrderingSpecipication.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using Core.Entities.OrderAggregate; 4 | 5 | namespace Core.Specification 6 | { 7 | public class OrderWithItemsAndOrderingSpecipication : BaseSpecipication 8 | { 9 | public OrderWithItemsAndOrderingSpecipication(string email) : base(o => o.BuyerEmail == email) 10 | { 11 | AddInclude(o => o.OrderItems); 12 | AddInclude(o => o.DeliveryMethod); 13 | AddOrderByDescending(o => o.OrderDate); 14 | } 15 | 16 | public OrderWithItemsAndOrderingSpecipication(int id, string email) : base(o => o.Id == id && o.BuyerEmail == email) 17 | { 18 | AddInclude(o => o.OrderItems); 19 | AddInclude(o => o.DeliveryMethod); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Core/Specification/ProductSpecParams.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Core.Specification 6 | { 7 | public class ProductSpecParams 8 | { 9 | private const int MaxPageSize = 50; 10 | public int PageIndex { get; set; } = 1; 11 | 12 | private int _pageSize = 6; 13 | 14 | public int PageSize 15 | { 16 | get => _pageSize; 17 | set => _pageSize = (value > MaxPageSize) ? MaxPageSize : value; 18 | } 19 | 20 | public int? BrandId { get; set; } 21 | public int? TypeId { get; set; } 22 | public string Sort { get; set; } 23 | private string _search; 24 | 25 | public string Search 26 | { 27 | get => _search; 28 | set => _search = value.ToLower(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Core/Specification/ProductWithFilterForCountSpecification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Core.Entities; 5 | 6 | namespace Core.Specification 7 | { 8 | public class ProductWithFilterForCountSpecification : BaseSpecipication 9 | { 10 | public ProductWithFilterForCountSpecification(ProductSpecParams productParams) 11 | : base(x => 12 | (String.IsNullOrEmpty(productParams.Search) || x.Name.ToLower().Contains(productParams.Search)) && 13 | (!productParams.BrandId.HasValue || x.ProductBrandId == productParams.BrandId) && 14 | (!productParams.TypeId.HasValue || x.ProductTypeId == productParams.TypeId) 15 | ) 16 | { 17 | 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Core/Specification/ProductWithTypesAndBrandsSpecification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Core.Entities; 5 | 6 | namespace Core.Specification 7 | { 8 | public class ProductWithTypesAndBrandsSpecification : BaseSpecipication 9 | { 10 | public ProductWithTypesAndBrandsSpecification(ProductSpecParams productParams) : base(x=> 11 | (String.IsNullOrEmpty(productParams.Search) || x.Name.ToLower().Contains(productParams.Search)) && 12 | (!productParams.BrandId.HasValue || x.ProductBrandId == productParams.BrandId) && 13 | (!productParams.TypeId.HasValue || x.ProductTypeId == productParams.TypeId) 14 | ) 15 | { 16 | AddInclude(x => x.ProductType); 17 | AddInclude(x => x.ProductBrand); 18 | AddOrderBy(x => x.Name); 19 | ApplyPaging(productParams.PageSize * (productParams.PageIndex -1), productParams.PageSize); 20 | 21 | if (!string.IsNullOrEmpty(productParams.Sort)) 22 | { 23 | switch (productParams.Sort) 24 | { 25 | case "priceAsc": 26 | AddOrderBy(p=>p.Price); 27 | break; 28 | case "priceDesc": 29 | AddOrderByDescending(p=>p.Price); 30 | break; 31 | default: 32 | AddOrderBy(n=>n.Name); 33 | break; 34 | } 35 | } 36 | } 37 | 38 | public ProductWithTypesAndBrandsSpecification(int id) : base(x => x.Id == id) 39 | { 40 | AddInclude(x => x.ProductType); 41 | AddInclude(x => x.ProductBrand); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Infrastructure/Data/BasketRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Text.Json; 5 | using System.Threading.Tasks; 6 | using Core.Entities; 7 | using Core.Interfaces; 8 | using StackExchange.Redis; 9 | 10 | namespace Infrastructure.Data 11 | { 12 | public class BasketRepository : IBasketRepository 13 | { 14 | private readonly IDatabase _database; 15 | public BasketRepository(IConnectionMultiplexer redis) 16 | { 17 | _database = redis.GetDatabase(); 18 | } 19 | public async Task GetBasketAsync(string basketId) 20 | { 21 | var data = await _database.StringGetAsync(basketId); 22 | 23 | return data.IsNullOrEmpty ? null : JsonSerializer.Deserialize(data); 24 | } 25 | 26 | public async Task UpdateBasketAsync(CustomerBasket basket) 27 | { 28 | var created = 29 | await _database.StringSetAsync(basket.Id, JsonSerializer.Serialize(basket), TimeSpan.FromDays(30)); 30 | 31 | if (!created) return null; 32 | 33 | return await GetBasketAsync(basket.Id); 34 | } 35 | 36 | public async Task DeleteBasketAsync(string id) 37 | { 38 | return await _database.KeyDeleteAsync(id); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Infrastructure/Data/Config/DeliveryMethodConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Core.Entities.OrderAggregate; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 7 | 8 | namespace Infrastructure.Data.Config 9 | { 10 | class DeliveryMethodConfiguration : IEntityTypeConfiguration 11 | { 12 | public void Configure(EntityTypeBuilder builder) 13 | { 14 | builder.Property(i => i.Price).HasColumnType("decimal(18.2)"); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Infrastructure/Data/Config/OrderConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Core.Entities.OrderAggregate; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 7 | 8 | namespace Infrastructure.Data.Config 9 | { 10 | class OrderConfiguration : IEntityTypeConfiguration 11 | { 12 | public void Configure(EntityTypeBuilder builder) 13 | { 14 | builder.OwnsOne(o => o.ShipToAddress, a => { a.WithOwner(); }); 15 | 16 | builder.Property(s => s.Status) 17 | .HasConversion( 18 | o => o.ToString(), 19 | o => (OrderStatus) Enum.Parse(typeof(OrderStatus), o)); 20 | 21 | builder.HasMany(o => o.OrderItems).WithOne().OnDelete(DeleteBehavior.Cascade); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Infrastructure/Data/Config/OrderItemConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Core.Entities.OrderAggregate; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 7 | 8 | namespace Infrastructure.Data.Config 9 | { 10 | class OrderItemConfiguration : IEntityTypeConfiguration 11 | { 12 | public void Configure(EntityTypeBuilder builder) 13 | { 14 | builder.OwnsOne(i => i.ItemOrdered, o => { o.WithOwner(); }); 15 | 16 | builder.Property(i => i.Price).HasColumnType("decimal(18.2)"); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Infrastructure/Data/Config/ProductConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Core.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace Infrastructure.Data.Config 6 | { 7 | public class ProductConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.Property(p => p.Id).IsRequired(); 12 | builder.Property(p => p.Name).IsRequired().HasMaxLength(100); 13 | builder.Property(p => p.Description).IsRequired(); 14 | builder.Property(p => p.Price).HasColumnType("decimal(18,2)"); 15 | builder.Property(p => p.PictureUrl).IsRequired(); 16 | builder.HasOne(b => b.ProductBrand).WithMany() 17 | .HasForeignKey(p => p.ProductBrandId); 18 | builder.HasOne(t => t.ProductType).WithMany() 19 | .HasForeignKey(p => p.ProductTypeId); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Infrastructure/Data/GenericRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Core.Entities; 7 | using Core.Interfaces; 8 | using Core.Specification; 9 | using Microsoft.EntityFrameworkCore; 10 | 11 | namespace Infrastructure.Data 12 | { 13 | public class GenericRepository : IGenericRepository where T : BaseEntity 14 | { 15 | private readonly StoreContext _context; 16 | 17 | public GenericRepository(StoreContext context) 18 | { 19 | _context = context; 20 | } 21 | 22 | public async Task GetByIdAsync(int id) 23 | { 24 | return await _context.Set().FindAsync(id); 25 | } 26 | 27 | public async Task> ListAllAsync() 28 | { 29 | return await _context.Set().ToListAsync(); 30 | } 31 | 32 | public async Task GetEntityWithSpec(ISpecification spec) 33 | { 34 | return await ApplySpecification(spec).FirstOrDefaultAsync(); 35 | } 36 | 37 | public async Task> ListAsync(ISpecification spec) 38 | { 39 | return await ApplySpecification(spec).ToListAsync(); 40 | } 41 | 42 | public async Task CountAsync(ISpecification spec) 43 | { 44 | return await ApplySpecification(spec).CountAsync(); 45 | } 46 | 47 | private IQueryable ApplySpecification(ISpecification spec) 48 | { 49 | return SpecificationEvaluator.GetQuery(_context.Set().AsQueryable(), spec); 50 | } 51 | 52 | public void Add(T entity) 53 | { 54 | _context.Set().Add(entity); 55 | } 56 | 57 | public void Update(T entity) 58 | { 59 | _context.Set().Attach(entity); 60 | _context.Entry(entity).State = EntityState.Modified; 61 | } 62 | 63 | public void Delete(T entity) 64 | { 65 | _context.Set().Remove(entity); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Infrastructure/Data/ProductRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Core.Entities; 4 | using Core.Interfaces; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace Infrastructure.Data 8 | { 9 | public class ProductRepository : IProductRepository 10 | { 11 | private readonly StoreContext _context; 12 | public ProductRepository(StoreContext context) 13 | { 14 | _context = context; 15 | } 16 | 17 | public async Task> GetProductBrandsAsync() 18 | { 19 | return await _context.ProductBrands.ToListAsync(); 20 | } 21 | 22 | public async Task GetProductByIdAsync(int id) 23 | { 24 | return await _context.Products 25 | .Include(p => p.ProductType) 26 | .Include(p => p.ProductBrand) 27 | .FirstOrDefaultAsync(p => p.Id == id); 28 | } 29 | 30 | public async Task> GetProductsAsync() 31 | { 32 | return await _context.Products 33 | .Include(p => p.ProductType) 34 | .Include(p => p.ProductBrand) 35 | .ToListAsync(); 36 | } 37 | 38 | public async Task> GetProductTypesAsync() 39 | { 40 | return await _context.ProductTypes.ToListAsync(); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Infrastructure/Data/SeedData/brands.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Id": 1, 4 | "Name": "Angular" 5 | }, 6 | { 7 | "Id": 2, 8 | "Name": "NetCore" 9 | }, 10 | { 11 | "Id": 3, 12 | "Name": "VS Code" 13 | }, 14 | { 15 | "Id": 4, 16 | "Name": "React" 17 | }, 18 | { 19 | "Id": 5, 20 | "Name": "Typescript" 21 | }, 22 | { 23 | "Id": 6, 24 | "Name": "Redis" 25 | } 26 | ] -------------------------------------------------------------------------------- /Infrastructure/Data/SeedData/delivery.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Id": 1, 4 | "ShortName": "UPS1", 5 | "Description": "Fastest delivery time", 6 | "DeliveryTime": "1-2 Days", 7 | "Price": 10 8 | }, 9 | { 10 | "Id": 2, 11 | "ShortName": "UPS2", 12 | "Description": "Get it within 5 days", 13 | "DeliveryTime": "2-5 Days", 14 | "Price": 5 15 | }, 16 | { 17 | "Id": 3, 18 | "ShortName": "UPS3", 19 | "Description": "Slower but cheap", 20 | "DeliveryTime": "5-10 Days", 21 | "Price": 2 22 | }, 23 | { 24 | "Id": 4, 25 | "ShortName": "FREE", 26 | "Description": "Free! You get what you pay for", 27 | "DeliveryTime": "1-2 Weeks", 28 | "Price": 0 29 | } 30 | ] -------------------------------------------------------------------------------- /Infrastructure/Data/SeedData/types.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Id": 1, 4 | "Name": "Boards" 5 | }, 6 | { 7 | "Id": 2, 8 | "Name": "Hats" 9 | }, 10 | { 11 | "Id": 3, 12 | "Name": "Boots" 13 | }, 14 | { 15 | "Id": 4, 16 | "Name": "Gloves" 17 | } 18 | ] -------------------------------------------------------------------------------- /Infrastructure/Data/SpecificationEvaluator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Core.Entities; 6 | using Core.Specification; 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | namespace Infrastructure.Data 10 | { 11 | class SpecificationEvaluator where TEntity: BaseEntity 12 | { 13 | public static IQueryable GetQuery(IQueryable inputQuery, ISpecification spec) 14 | { 15 | var query = inputQuery; 16 | 17 | if (spec.Criteria != null) 18 | { 19 | query = query.Where(spec.Criteria); 20 | } 21 | 22 | if (spec.OrderBy != null) 23 | { 24 | query = query.OrderBy(spec.OrderBy); 25 | } 26 | 27 | if (spec.OrderByDescending != null) 28 | { 29 | query = query.OrderByDescending(spec.OrderByDescending); 30 | } 31 | 32 | if (spec.IsPagingEnabled) 33 | { 34 | query = query.Skip(spec.Skip).Take(spec.Take); 35 | } 36 | 37 | query = spec.Includes.Aggregate(query, (current, include) => current.Include(include)); 38 | 39 | return query; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Infrastructure/Data/StoreContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using Core.Entities; 5 | using Core.Entities.OrderAggregate; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace Infrastructure.Data 10 | { 11 | public class StoreContext : DbContext 12 | { 13 | public StoreContext(DbContextOptions options) : base(options) 14 | { 15 | } 16 | 17 | public DbSet Products { get; set; } 18 | public DbSet ProductBrands { get; set; } 19 | public DbSet ProductTypes { get; set; } 20 | public DbSet Orders { get; set; } 21 | public DbSet OrderItems { get; set; } 22 | public DbSet DeliveryMethods { get; set; } 23 | 24 | protected override void OnModelCreating(ModelBuilder modelBuilder) 25 | { 26 | base.OnModelCreating(modelBuilder); 27 | modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); 28 | 29 | if (Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite") 30 | { 31 | foreach (var entityType in modelBuilder.Model.GetEntityTypes()) 32 | { 33 | var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(decimal)); 34 | 35 | var dateTimeProperties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset)); 36 | 37 | foreach (var property in properties) 38 | { 39 | modelBuilder.Entity(entityType.Name).Property(property.Name).HasConversion(); 40 | } 41 | 42 | foreach(var property in dateTimeProperties) 43 | { 44 | modelBuilder.Entity(entityType.Name).Property(property.Name).HasConversion(new DateTimeOffsetToBinaryConverter()); 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /Infrastructure/Data/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Threading.Tasks; 4 | using Core.Entities; 5 | using Core.Interfaces; 6 | 7 | namespace Infrastructure.Data 8 | { 9 | public class UnitOfWork : IUnitOfWork 10 | { 11 | private readonly StoreContext _context; 12 | private Hashtable _repositories; 13 | public UnitOfWork(StoreContext context) 14 | { 15 | _context = context; 16 | } 17 | 18 | public async Task Complete() 19 | { 20 | return await _context.SaveChangesAsync(); 21 | } 22 | 23 | public void Dispose() 24 | { 25 | _context.Dispose(); 26 | } 27 | 28 | public IGenericRepository Repository() where TEntity : BaseEntity 29 | { 30 | if(_repositories == null) _repositories = new Hashtable(); 31 | 32 | var type = typeof(TEntity).Name; 33 | 34 | if(!_repositories.ContainsKey(type)) 35 | { 36 | var repositoryType = typeof(GenericRepository<>); 37 | var repositoryInstance = Activator.CreateInstance(repositoryType.MakeGenericType(typeof(TEntity)), _context); 38 | 39 | _repositories.Add(type, repositoryInstance); 40 | } 41 | 42 | return (IGenericRepository) _repositories[type]; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /Infrastructure/Identity/AppIdentityDbContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Core.Entities.Identity; 5 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | namespace Infrastructure.Identity 9 | { 10 | public class AppIdentityDbContext : IdentityDbContext 11 | { 12 | public AppIdentityDbContext(DbContextOptions options) : base(options) 13 | { 14 | 15 | } 16 | 17 | protected override void OnModelCreating(ModelBuilder builder) 18 | { 19 | base.OnModelCreating(builder); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Infrastructure/Identity/AppIdentityDbContextSeed.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Core.Entities.Identity; 4 | using Microsoft.AspNetCore.Identity; 5 | 6 | namespace Infrastructure.Identity 7 | { 8 | public class AppIdentityDbContextSeed 9 | { 10 | public static async Task SeedUserAsync(UserManager userManager){ 11 | if(!userManager.Users.Any()){ 12 | var user = new AppUser{ 13 | DisplayName = "Robin", 14 | Email = "robin@test.com", 15 | UserName = "robin@test.com", 16 | Address = new Address{ 17 | FirstName = "Robin", 18 | LastName = "Haider", 19 | Street = "10 The Street", 20 | City = "New York", 21 | State = "NY", 22 | Zipcode = "902120" 23 | } 24 | }; 25 | await userManager.CreateAsync(user, "Pa$$w0rd"); 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Infrastructure/Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | netstandard2.1 22 | 23 | -------------------------------------------------------------------------------- /Infrastructure/Services/ResponseCacheService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Threading.Tasks; 4 | using Core.Interfaces; 5 | using StackExchange.Redis; 6 | 7 | namespace Infrastructure.Services 8 | { 9 | public class ResponseCacheService : IResponseCacheService 10 | { 11 | private readonly IDatabase _database; 12 | public ResponseCacheService(IConnectionMultiplexer redis) 13 | { 14 | _database = redis.GetDatabase(); 15 | } 16 | 17 | public async Task CacheResponseAsync(string cacheKey, object response, TimeSpan timeToLive) 18 | { 19 | if(response == null) 20 | { 21 | return; 22 | } 23 | 24 | var options = new JsonSerializerOptions 25 | { 26 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase 27 | }; 28 | 29 | var serializedResponse = JsonSerializer.Serialize(response, options); 30 | 31 | await _database.StringSetAsync(cacheKey, serializedResponse, timeToLive); 32 | } 33 | 34 | public async Task GetCacheResponseAsync(string cacheKey) 35 | { 36 | var cachedResponse = await _database.StringGetAsync(cacheKey); 37 | 38 | if(cachedResponse.IsNullOrEmpty) 39 | { 40 | return null; 41 | } 42 | 43 | return cachedResponse; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Infrastructure/Services/TokenService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IdentityModel.Tokens.Jwt; 4 | using System.Security.Claims; 5 | using System.Text; 6 | using Core.Entities.Identity; 7 | using Core.Interfaces; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.IdentityModel.Tokens; 10 | 11 | namespace Infrastructure.Services 12 | { 13 | public class TokenService : ITokenService 14 | { 15 | private readonly IConfiguration _config; 16 | private readonly SymmetricSecurityKey _key; 17 | public TokenService(IConfiguration config) 18 | { 19 | _config = config; 20 | _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Token:Key"])); 21 | } 22 | 23 | public string CreateToken(AppUser user) 24 | { 25 | var claims = new List{ 26 | new Claim(JwtRegisteredClaimNames.Email, user.Email), 27 | new Claim(JwtRegisteredClaimNames.GivenName, user.DisplayName) 28 | }; 29 | 30 | var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature); 31 | 32 | var tokenDescriptor = new SecurityTokenDescriptor{ 33 | Subject = new ClaimsIdentity(claims), 34 | Expires = DateTime.Now.AddDays(7), 35 | SigningCredentials = creds, 36 | Issuer = _config["Token:Issuer"] 37 | }; 38 | 39 | var tokenHandler = new JwtSecurityTokenHandler(); 40 | 41 | var token = tokenHandler.CreateToken(tokenDescriptor); 42 | 43 | return tokenHandler.WriteToken(token); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # An-E-commerce-app-with-.Net-Core-and-Angular 2 | Learning to build an e-commerce app with .Net Core and Angular 3 | 4 | A concept e-commerce store using Angular, .Net Core and Stripe for payment processing. I build this project through the course from udemy "Learn to build an e-commerce app with .Net Core and Angular" by Neil Cummings. 5 | 6 | # Used in this Project 7 | .Net Core 8 | 9 | Angular 10 | 11 | C# Generics 12 | 13 | Repository and Unit of Work Pattern 14 | 15 | Specification Pattern 16 | 17 | Caching 18 | 19 | Angular Lazy loading 20 | 21 | Angular Routing 22 | 23 | Angular Reactive Forms 24 | 25 | Angular Creating a MultiStep form wizard 26 | 27 | Accepting payments using Stripe 28 | 29 | Angular Re-usable form components 30 | 31 | Angular validation and async validation 32 | 33 | # Here are some of the things I learned from this project: 34 | 35 | Setting up the developer environment 36 | 37 | Creating a multi project .net core application using the dotnet CLI 38 | 39 | Creating a client side front-end Angular UI for the store using the Angular CLI 40 | 41 | Learn how to use the Repository, Unit of Work and specification pattern in .net core 42 | 43 | Using multiple DbContext as context boundaries 44 | 45 | Using ASP.NET Identity for login and registration 46 | 47 | Using the angular modules to create lazy loaded routes. 48 | 49 | Using Automapper in ASP.NET Core 50 | 51 | Building a great looking UI using Bootstrap 52 | 53 | Making reusable form components using Angular Reactive forms 54 | 55 | Paging, Sorting, Searching and Filtering 56 | 57 | Using Redis to store the shopping basket 58 | 59 | Creating orders from the shopping basket 60 | 61 | Accepting payments via Stripe using the new EU standards for 3D secure 62 | 63 | Publishing the application to Linux 64 | 65 | Many more things as well 66 | -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | ssl 3 | 4 | # compiled output 5 | /dist 6 | /tmp 7 | /out-tsc 8 | # Only exists if Bazel was run 9 | /bazel-out 10 | 11 | # dependencies 12 | /node_modules 13 | 14 | # profiling files 15 | chrome-profiler-events*.json 16 | speed-measure-plugin*.json 17 | 18 | # IDEs and editors 19 | /.idea 20 | .project 21 | .classpath 22 | .c9/ 23 | *.launch 24 | .settings/ 25 | *.sublime-workspace 26 | 27 | # IDE - VSCode 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | .history/* 34 | 35 | # misc 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Client 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.1.3. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /client/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /client/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /client/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('client app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/client'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~9.1.3", 15 | "@angular/cdk": "^9.2.4", 16 | "@angular/common": "~9.1.3", 17 | "@angular/compiler": "~9.1.3", 18 | "@angular/core": "~9.1.3", 19 | "@angular/forms": "~9.1.3", 20 | "@angular/platform-browser": "~9.1.3", 21 | "@angular/platform-browser-dynamic": "~9.1.3", 22 | "@angular/router": "~9.1.3", 23 | "bootstrap": "4.3.1", 24 | "bootswatch": "^4.4.1", 25 | "font-awesome": "^4.7.0", 26 | "ngx-bootstrap": "^5.6.1", 27 | "ngx-spinner": "^9.0.2", 28 | "ngx-toastr": "^12.0.1", 29 | "rxjs": "~6.5.4", 30 | "tslib": "^1.10.0", 31 | "uuid": "^8.0.0", 32 | "xng-breadcrumb": "^5.0.1", 33 | "zone.js": "~0.10.2" 34 | }, 35 | "devDependencies": { 36 | "@angular-devkit/build-angular": "~0.901.3", 37 | "@angular/cli": "~9.1.3", 38 | "@angular/compiler-cli": "~9.1.3", 39 | "@angular/language-service": "~9.1.3", 40 | "@types/node": "^12.11.1", 41 | "@types/jasmine": "~3.5.0", 42 | "@types/jasminewd2": "~2.0.3", 43 | "codelyzer": "^5.1.2", 44 | "jasmine-core": "~3.5.0", 45 | "jasmine-spec-reporter": "~4.2.1", 46 | "karma": "~5.0.0", 47 | "karma-chrome-launcher": "~3.1.0", 48 | "karma-coverage-istanbul-reporter": "~2.1.0", 49 | "karma-jasmine": "~3.0.1", 50 | "karma-jasmine-html-reporter": "^1.4.2", 51 | "protractor": "~5.4.3", 52 | "ts-node": "~8.3.0", 53 | "tslint": "~6.1.0", 54 | "typescript": "~3.8.3" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /client/src/app/account/account-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { RegisterComponent } from './register/register.component'; 2 | import { LoginComponent } from './login/login.component'; 3 | import { NgModule } from '@angular/core'; 4 | import { Routes, RouterModule } from '@angular/router'; 5 | 6 | 7 | const routes: Routes = [ 8 | {path: 'login', component: LoginComponent}, 9 | {path: 'register', component: RegisterComponent} 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class AccountRoutingModule { } 17 | -------------------------------------------------------------------------------- /client/src/app/account/account.module.ts: -------------------------------------------------------------------------------- 1 | import { SharedModule } from './../shared/shared.module'; 2 | import { NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | 5 | import { AccountRoutingModule } from './account-routing.module'; 6 | import { LoginComponent } from './login/login.component'; 7 | import { RegisterComponent } from './register/register.component'; 8 | 9 | 10 | @NgModule({ 11 | declarations: [LoginComponent, RegisterComponent], 12 | imports: [ 13 | CommonModule, 14 | AccountRoutingModule, 15 | SharedModule 16 | ] 17 | }) 18 | export class AccountModule { } 19 | -------------------------------------------------------------------------------- /client/src/app/account/account.service.ts: -------------------------------------------------------------------------------- 1 | import { Address } from './../shared/models/address'; 2 | import { User } from './../shared/models/user'; 3 | import { BehaviorSubject, ReplaySubject, of } from 'rxjs'; 4 | import { environment } from './../../environments/environment'; 5 | import { Injectable } from '@angular/core'; 6 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 7 | import { Router } from '@angular/router'; 8 | import { map } from 'rxjs/operators'; 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class AccountService { 14 | baseUrl = environment.apiUrl; 15 | private currentUserSource = new ReplaySubject(1); 16 | currentUser$ = this.currentUserSource.asObservable(); 17 | 18 | constructor(private http: HttpClient, private router: Router) { } 19 | 20 | 21 | 22 | loadCurrentUser(token: string){ 23 | if(token === null){ 24 | this.currentUserSource.next(null); 25 | return of(null); 26 | } 27 | 28 | let headers = new HttpHeaders(); 29 | headers = headers.set('Authorization', `Bearer ${token}`); 30 | 31 | return this.http.get(this.baseUrl + 'account', {headers}).pipe( 32 | map((user: User) => { 33 | if(user){ 34 | localStorage.setItem('token', user.token); 35 | this.currentUserSource.next(user); 36 | } 37 | }) 38 | ); 39 | } 40 | 41 | login(values: any){ 42 | return this.http.post(this.baseUrl + 'account/login', values).pipe( 43 | map((user: User) => { 44 | if(user){ 45 | localStorage.setItem('token', user.token); 46 | this.currentUserSource.next(user); 47 | } 48 | }) 49 | ); 50 | } 51 | 52 | register(values: any){ 53 | return this.http.post(this.baseUrl + 'account/register', values).pipe( 54 | map((user: User) => { 55 | if(user){ 56 | localStorage.setItem('token', user.token); 57 | this.currentUserSource.next(user); 58 | } 59 | }) 60 | ); 61 | } 62 | 63 | logout(){ 64 | localStorage.removeItem('token'); 65 | this.currentUserSource.next(null); 66 | this.router.navigateByUrl('/'); 67 | } 68 | 69 | checkEmailExists(email: string){ 70 | return this.http.get(this.baseUrl + 'account/emailexists?email=' + email); 71 | } 72 | 73 | getUserAddress(){ 74 | return this.http.get
(this.baseUrl + 'account/address'); 75 | } 76 | 77 | updateUserAddress(address: Address){ 78 | return this.http.put
(this.baseUrl + 'account/address', address); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /client/src/app/account/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 22 |
23 |
-------------------------------------------------------------------------------- /client/src/app/account/login/login.component.scss: -------------------------------------------------------------------------------- 1 | .form-label-group { 2 | position: relative; 3 | margin-bottom: 1rem; 4 | } 5 | 6 | .form-label-group > input, 7 | .form-label-group > label { 8 | height: 3.125rem; 9 | padding: .75rem; 10 | } 11 | 12 | .form-label-group > label { 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | display: block; 17 | width: 100%; 18 | margin-bottom: 0; /* Override default `