├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── QuickApp.Core ├── Extensions │ ├── ArrayExtensions.cs │ └── StringExtensions.cs ├── Infrastructure │ ├── ApplicationDbContext.cs │ ├── DatabaseSeeder.cs │ └── IDatabaseSeeder.cs ├── Models │ ├── Account │ │ ├── ApplicationPermission.cs │ │ ├── ApplicationRole.cs │ │ └── ApplicationUser.cs │ ├── BaseEntity.cs │ ├── Gender.cs │ ├── IAuditableEntity.cs │ └── Shop │ │ ├── Customer.cs │ │ ├── Order.cs │ │ ├── OrderDetail.cs │ │ ├── Product.cs │ │ └── ProductCategory.cs ├── QuickApp.Core.csproj └── Services │ ├── Account │ ├── ApplicationPermissions.cs │ ├── CustomClaims.cs │ ├── Exceptions │ │ ├── UserAccountException.cs │ │ ├── UserNotFoundException.cs │ │ └── UserRoleException.cs │ ├── Interfaces │ │ ├── IUserAccountService.cs │ │ ├── IUserIdAccessor.cs │ │ └── IUserRoleService.cs │ ├── UserAccountService.cs │ └── UserRoleService.cs │ ├── IEmailSender.cs │ └── Shop │ ├── CustomerService.cs │ ├── Exceptions │ └── CustomerException.cs │ ├── Interfaces │ ├── ICustomerService.cs │ ├── IOrdersService.cs │ └── IProductService.cs │ ├── OrdersService.cs │ └── ProductService.cs ├── QuickApp.Server ├── Attributes │ ├── MinimumCountAttribute.cs │ └── SanitizeModelAttribute.cs ├── Authorization │ ├── AuthPolicies.cs │ ├── Requirements │ │ ├── AssignRolesAuthorizationRequirement.cs │ │ ├── UserAccountAuthorizationRequirement.cs │ │ └── ViewRoleAuthorizationRequirement.cs │ ├── SwaggerAuthorizeOperationFilter.cs │ └── UserAccountManagementOperations.cs ├── Configuration │ ├── AppSettings.cs │ ├── DesignTimeDbContextFactory.cs │ ├── MappingProfile.cs │ └── OidcServerConfig.cs ├── Controllers │ ├── AuthorizationController.cs │ ├── BaseApiController.cs │ ├── CustomerController.cs │ ├── UserAccountController.cs │ └── UserRoleController.cs ├── Migrations │ ├── 20241114121916_Initial.Designer.cs │ ├── 20241114121916_Initial.cs │ └── ApplicationDbContextModelSnapshot.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── QuickApp.Server.csproj ├── QuickApp.Server.http ├── Services │ ├── Email │ │ ├── EmailSender.cs │ │ ├── EmailTemplates.cs │ │ ├── PlainTextTestEmail.template │ │ └── TestEmail.template │ ├── UserIdAccessor.cs │ └── Utilities.cs ├── ViewModels │ ├── Account │ │ ├── ClaimVM.cs │ │ ├── PermissionVM.cs │ │ ├── RoleVM.cs │ │ └── UserVMs.cs │ └── Shop │ │ ├── CustomerVM.cs │ │ ├── OrderVM.cs │ │ └── ProductVM.cs ├── appsettings.Development.json └── appsettings.json ├── QuickApp.sln ├── README.md └── quickapp.client ├── .editorconfig ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── README.md ├── angular.json ├── aspnetcore-https.js ├── eslint.config.js ├── karma.conf.js ├── nuget.config ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── images │ ├── demo │ │ ├── banner1.png │ │ ├── banner2.png │ │ ├── banner3.png │ │ └── banner4.png │ ├── logo-black.png │ └── logo-white.png ├── locale │ ├── ar.json │ ├── de.json │ ├── en.json │ ├── es.json │ ├── fr.json │ ├── ko.json │ ├── pt.json │ └── zh.json └── styles-external.css ├── quickapp.client.esproj ├── src ├── app │ ├── app-error.handler.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.config.ts │ ├── app.routes.ts │ ├── assets │ │ ├── scripts │ │ │ └── alertify.js │ │ ├── styles │ │ │ ├── _custom-variables.scss │ │ │ ├── alertify.bootstrap.css │ │ │ ├── alertify.core.css │ │ │ ├── alertify.default.css │ │ │ ├── ngx-datatable.scss │ │ │ ├── styles-vendor.scss │ │ │ └── vertical-tabs.scss │ │ └── themes │ │ │ ├── _app-theme.scss │ │ │ ├── bootswatch_fix │ │ │ ├── _cosmo.scss │ │ │ ├── _flatly.scss │ │ │ ├── _journal.scss │ │ │ ├── _lumen.scss │ │ │ ├── _minty.scss │ │ │ ├── _sketchy.scss │ │ │ ├── _solar.scss │ │ │ ├── _spacelab.scss │ │ │ ├── _superhero.scss │ │ │ └── _united.scss │ │ │ ├── cerulean.scss │ │ │ ├── cosmo.scss │ │ │ ├── flatly.scss │ │ │ ├── journal.scss │ │ │ ├── lumen.scss │ │ │ ├── minty.scss │ │ │ ├── pulse.scss │ │ │ ├── sketchy.scss │ │ │ ├── slate.scss │ │ │ ├── solar.scss │ │ │ ├── spacelab.scss │ │ │ ├── superhero.scss │ │ │ └── united.scss │ ├── components │ │ ├── about │ │ │ ├── about.component.html │ │ │ ├── about.component.scss │ │ │ └── about.component.ts │ │ ├── controls │ │ │ ├── banner-demo.component.html │ │ │ ├── banner-demo.component.scss │ │ │ ├── banner-demo.component.ts │ │ │ ├── notifications-viewer.component.html │ │ │ ├── notifications-viewer.component.scss │ │ │ ├── notifications-viewer.component.ts │ │ │ ├── role-editor.component.html │ │ │ ├── role-editor.component.scss │ │ │ ├── role-editor.component.ts │ │ │ ├── roles-management.component.html │ │ │ ├── roles-management.component.scss │ │ │ ├── roles-management.component.ts │ │ │ ├── search-box.component.html │ │ │ ├── search-box.component.scss │ │ │ ├── search-box.component.ts │ │ │ ├── statistics-demo.component.html │ │ │ ├── statistics-demo.component.scss │ │ │ ├── statistics-demo.component.ts │ │ │ ├── todo-demo.component.html │ │ │ ├── todo-demo.component.scss │ │ │ ├── todo-demo.component.ts │ │ │ ├── user-info.component.html │ │ │ ├── user-info.component.scss │ │ │ ├── user-info.component.ts │ │ │ ├── user-preferences.component.html │ │ │ ├── user-preferences.component.scss │ │ │ ├── user-preferences.component.ts │ │ │ ├── users-management.component.html │ │ │ ├── users-management.component.scss │ │ │ └── users-management.component.ts │ │ ├── customers │ │ │ ├── customers.component.html │ │ │ ├── customers.component.scss │ │ │ └── customers.component.ts │ │ ├── home │ │ │ ├── home.component.html │ │ │ ├── home.component.scss │ │ │ └── home.component.ts │ │ ├── login │ │ │ ├── login.component.html │ │ │ ├── login.component.scss │ │ │ └── login.component.ts │ │ ├── not-found │ │ │ ├── not-found.component.html │ │ │ ├── not-found.component.scss │ │ │ └── not-found.component.ts │ │ ├── orders │ │ │ ├── orders.component.html │ │ │ ├── orders.component.scss │ │ │ └── orders.component.ts │ │ ├── products │ │ │ ├── products.component.html │ │ │ ├── products.component.scss │ │ │ └── products.component.ts │ │ └── settings │ │ │ ├── settings.component.html │ │ │ ├── settings.component.scss │ │ │ └── settings.component.ts │ ├── directives │ │ ├── autofocus.directive.ts │ │ └── equal-validator.directive.ts │ ├── models │ │ ├── Alertify.ts │ │ ├── app-theme.model.ts │ │ ├── enums.ts │ │ ├── environment.model.ts │ │ ├── login-response.model.ts │ │ ├── notification.model.ts │ │ ├── permission.model.ts │ │ ├── role.model.ts │ │ ├── user-edit.model.ts │ │ ├── user-login.model.ts │ │ └── user.model.ts │ ├── pipes │ │ └── group-by.pipe.ts │ └── services │ │ ├── account-endpoint.service.ts │ │ ├── account.service.ts │ │ ├── alert.service.ts │ │ ├── animations.ts │ │ ├── app-title.service.ts │ │ ├── app-translation.service.ts │ │ ├── auth-guard.ts │ │ ├── auth.service.ts │ │ ├── configuration.service.ts │ │ ├── db-keys.ts │ │ ├── endpoint-base.service.ts │ │ ├── jwt-helper.ts │ │ ├── local-store-manager.service.ts │ │ ├── lowercase-url-serializer.service.ts │ │ ├── notification-endpoint.service.ts │ │ ├── notification.service.ts │ │ ├── oidc-helper.service.ts │ │ ├── theme-manager.ts │ │ └── utilities.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── index.html ├── main.ts ├── proxy.conf.js └── styles.scss ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: emonney -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ebenezer Monney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /QuickApp.Core/Extensions/ArrayExtensions.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Extensions 8 | { 9 | public static class ArrayExtensions 10 | { 11 | public static T[]? NullIfEmpty(this T[]? value) => value?.Length == 0 ? null : value; 12 | 13 | public static T[]? EmptyIfNull(this T[]? value) => value ?? []; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /QuickApp.Core/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Extensions 8 | { 9 | public static class StringExtensions 10 | { 11 | public static string? NullIfWhiteSpace(this string? value) => string.IsNullOrWhiteSpace(value) ? null : value; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /QuickApp.Core/Infrastructure/IDatabaseSeeder.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Infrastructure 8 | { 9 | public interface IDatabaseSeeder 10 | { 11 | Task SeedAsync(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /QuickApp.Core/Models/Account/ApplicationPermission.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using System.Diagnostics.CodeAnalysis; 8 | 9 | namespace QuickApp.Core.Models.Account 10 | { 11 | public class ApplicationPermission(string name, string value, string groupName, string? description = null) 12 | { 13 | public string Name { get; set; } = name; 14 | public string Value { get; set; } = value; 15 | public string GroupName { get; set; } = groupName; 16 | public string? Description { get; set; } = description; 17 | 18 | public override string ToString() => Value; 19 | 20 | [return: NotNullIfNotNull(nameof(permission))] 21 | public static implicit operator string?(ApplicationPermission? permission) 22 | { 23 | return permission?.Value; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /QuickApp.Core/Models/Account/ApplicationRole.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using Microsoft.AspNetCore.Identity; 8 | 9 | namespace QuickApp.Core.Models.Account 10 | { 11 | public class ApplicationRole : IdentityRole, IAuditableEntity 12 | { 13 | /// 14 | /// Initializes a new instance of . 15 | /// 16 | /// 17 | /// The Id property is initialized to from a new GUID string value. 18 | /// 19 | public ApplicationRole() 20 | { } 21 | 22 | /// 23 | /// Initializes a new instance of . 24 | /// 25 | /// The role name. 26 | /// 27 | /// The Id property is initialized to from a new GUID string value. 28 | /// 29 | public ApplicationRole(string roleName) : base(roleName) 30 | { } 31 | 32 | /// 33 | /// Initializes a new instance of . 34 | /// 35 | /// The role name. 36 | /// Description of the role. 37 | /// 38 | /// The Id property is initialized to from a new GUID string value. 39 | /// 40 | public ApplicationRole(string roleName, string description) : base(roleName) 41 | { 42 | Description = description; 43 | } 44 | 45 | /// 46 | /// Gets or sets the description for this role. 47 | /// 48 | public string? Description { get; set; } 49 | public string? CreatedBy { get; set; } 50 | public string? UpdatedBy { get; set; } 51 | public DateTime CreatedDate { get; set; } 52 | public DateTime UpdatedDate { get; set; } 53 | 54 | /// 55 | /// Navigation property for the users in this role. 56 | /// 57 | public ICollection> Users { get; } = []; 58 | 59 | /// 60 | /// Navigation property for claims in this role. 61 | /// 62 | public ICollection> Claims { get; } = []; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /QuickApp.Core/Models/Account/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using Microsoft.AspNetCore.Identity; 8 | using QuickApp.Core.Models.Shop; 9 | 10 | namespace QuickApp.Core.Models.Account 11 | { 12 | public class ApplicationUser : IdentityUser, IAuditableEntity 13 | { 14 | public virtual string? FriendlyName 15 | { 16 | get 17 | { 18 | var friendlyName = string.IsNullOrWhiteSpace(FullName) ? UserName : FullName; 19 | 20 | if (!string.IsNullOrWhiteSpace(JobTitle)) 21 | friendlyName = $"{JobTitle} {friendlyName}"; 22 | 23 | return friendlyName; 24 | } 25 | } 26 | 27 | public string? JobTitle { get; set; } 28 | public string? FullName { get; set; } 29 | public string? Configuration { get; set; } 30 | public bool IsEnabled { get; set; } 31 | public bool IsLockedOut => LockoutEnabled && LockoutEnd >= DateTimeOffset.UtcNow; 32 | 33 | public string? CreatedBy { get; set; } 34 | public string? UpdatedBy { get; set; } 35 | public DateTime CreatedDate { get; set; } 36 | public DateTime UpdatedDate { get; set; } 37 | 38 | /// 39 | /// Navigation property for the roles this user belongs to. 40 | /// 41 | public ICollection> Roles { get; } = []; 42 | 43 | /// 44 | /// Navigation property for the claims this user possesses. 45 | /// 46 | public ICollection> Claims { get; } = []; 47 | 48 | /// 49 | /// Demo Navigation property for orders this user has processed 50 | /// 51 | public ICollection Orders { get; } = []; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /QuickApp.Core/Models/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using System.ComponentModel.DataAnnotations; 8 | 9 | namespace QuickApp.Core.Models 10 | { 11 | public class BaseEntity : IAuditableEntity 12 | { 13 | public int Id { get; set; } 14 | 15 | [MaxLength(40)] 16 | public string? CreatedBy { get; set; } 17 | 18 | [MaxLength(40)] 19 | public string? UpdatedBy { get; set; } 20 | 21 | public DateTime UpdatedDate { get; set; } 22 | 23 | public DateTime CreatedDate { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /QuickApp.Core/Models/Gender.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Models 8 | { 9 | public enum Gender 10 | { 11 | None, 12 | Female, 13 | Male 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /QuickApp.Core/Models/IAuditableEntity.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Models 8 | { 9 | public interface IAuditableEntity 10 | { 11 | string? CreatedBy { get; set; } 12 | string? UpdatedBy { get; set; } 13 | DateTime CreatedDate { get; set; } 14 | DateTime UpdatedDate { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /QuickApp.Core/Models/Shop/Customer.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Models.Shop 8 | { 9 | public class Customer : BaseEntity 10 | { 11 | public required string Name { get; set; } 12 | public required string Email { get; set; } 13 | public string? PhoneNumber { get; set; } 14 | public string? Address { get; set; } 15 | public string? City { get; set; } 16 | public Gender Gender { get; set; } 17 | 18 | public ICollection Orders { get; } = []; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /QuickApp.Core/Models/Shop/Order.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using QuickApp.Core.Models.Account; 8 | 9 | namespace QuickApp.Core.Models.Shop 10 | { 11 | public class Order : BaseEntity 12 | { 13 | public decimal Discount { get; set; } 14 | public string? Comments { get; set; } 15 | 16 | public string? CashierId { get; set; } 17 | public ApplicationUser? Cashier { get; set; } 18 | 19 | public int CustomerId { get; set; } 20 | public required Customer Customer { get; set; } 21 | 22 | public ICollection OrderDetails { get; } = []; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /QuickApp.Core/Models/Shop/OrderDetail.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Models.Shop 8 | { 9 | public class OrderDetail : BaseEntity 10 | { 11 | public decimal UnitPrice { get; set; } 12 | public int Quantity { get; set; } 13 | public decimal Discount { get; set; } 14 | 15 | public int ProductId { get; set; } 16 | public required Product Product { get; set; } 17 | 18 | public int OrderId { get; set; } 19 | public required Order Order { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /QuickApp.Core/Models/Shop/Product.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Models.Shop 8 | { 9 | public class Product : BaseEntity 10 | { 11 | public required string Name { get; set; } 12 | public string? Description { get; set; } 13 | public string? Icon { get; set; } 14 | public decimal BuyingPrice { get; set; } 15 | public decimal SellingPrice { get; set; } 16 | public int UnitsInStock { get; set; } 17 | public bool IsActive { get; set; } 18 | public bool IsDiscontinued { get; set; } 19 | 20 | public int? ParentId { get; set; } 21 | public Product? Parent { get; set; } 22 | 23 | public int ProductCategoryId { get; set; } 24 | public required ProductCategory ProductCategory { get; set; } 25 | 26 | public ICollection Children { get; } = []; 27 | public ICollection OrderDetails { get; } = []; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /QuickApp.Core/Models/Shop/ProductCategory.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Models.Shop 8 | { 9 | public class ProductCategory : BaseEntity 10 | { 11 | public required string Name { get; set; } 12 | public string? Description { get; set; } 13 | public string? Icon { get; set; } 14 | 15 | public ICollection Products { get; } = []; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /QuickApp.Core/QuickApp.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 9.19.0 8 | www.ebenmonney.com 9 | $(Company) 10 | Data Access Layer for the Quick Application template 11 | Copyright © 2024 www.ebenmonney.com 12 | https://www.ebenmonney.com/quickapp 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /QuickApp.Core/Services/Account/ApplicationPermissions.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using QuickApp.Core.Models.Account; 8 | using System.Collections.ObjectModel; 9 | 10 | namespace QuickApp.Core.Services.Account 11 | { 12 | public static class ApplicationPermissions 13 | { 14 | /************* USER PERMISSIONS *************/ 15 | 16 | public const string UsersPermissionGroupName = "User Permissions"; 17 | 18 | public static readonly ApplicationPermission ViewUsers = new( 19 | "View Users", 20 | "users.view", 21 | UsersPermissionGroupName, 22 | "Permission to view other users account details"); 23 | 24 | public static readonly ApplicationPermission ManageUsers = new( 25 | "Manage Users", 26 | "users.manage", 27 | UsersPermissionGroupName, 28 | "Permission to create, delete and modify other users account details"); 29 | 30 | /************* ROLE PERMISSIONS *************/ 31 | 32 | public const string RolesPermissionGroupName = "Role Permissions"; 33 | 34 | public static readonly ApplicationPermission ViewRoles = new( 35 | "View Roles", 36 | "roles.view", 37 | RolesPermissionGroupName, 38 | "Permission to view available roles"); 39 | 40 | public static readonly ApplicationPermission ManageRoles = new( 41 | "Manage Roles", 42 | "roles.manage", 43 | RolesPermissionGroupName, 44 | "Permission to create, delete and modify roles"); 45 | 46 | public static readonly ApplicationPermission AssignRoles = new( 47 | "Assign Roles", 48 | "roles.assign", 49 | RolesPermissionGroupName, 50 | "Permission to assign roles to users"); 51 | 52 | /************* ALL PERMISSIONS *************/ 53 | 54 | public static readonly ReadOnlyCollection AllPermissions = 55 | new List { 56 | ViewUsers, ManageUsers, 57 | ViewRoles, ManageRoles, AssignRoles 58 | }.AsReadOnly(); 59 | 60 | /************* HELPER METHODS *************/ 61 | 62 | public static ApplicationPermission? GetPermissionByName(string? permissionName) 63 | { 64 | return AllPermissions.SingleOrDefault(p => p.Name == permissionName); 65 | } 66 | 67 | public static ApplicationPermission? GetPermissionByValue(string? permissionValue) 68 | { 69 | return AllPermissions.SingleOrDefault(p => p.Value == permissionValue); 70 | } 71 | 72 | public static string[] GetAllPermissionValues() 73 | { 74 | return AllPermissions.Select(p => p.Value).ToArray(); 75 | } 76 | 77 | public static string[] GetAdministrativePermissionValues() 78 | { 79 | return [ManageUsers, ManageRoles, AssignRoles]; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /QuickApp.Core/Services/Account/CustomClaims.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Services.Account 8 | { 9 | public static class CustomClaims 10 | { 11 | ///A claim that specifies the full name of an entity 12 | public const string FullName = "fullname"; 13 | 14 | ///A claim that specifies the job title of an entity 15 | public const string JobTitle = "jobtitle"; 16 | 17 | ///A claim that specifies the configuration/customizations of an entity 18 | public const string Configuration = "configuration"; 19 | 20 | ///A claim that specifies the permission of an entity 21 | public const string Permission = "permission"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /QuickApp.Core/Services/Account/Exceptions/UserAccountException.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Services.Account 8 | { 9 | /// 10 | /// Represents errors that occur with user account related operations. 11 | /// 12 | public class UserAccountException : Exception 13 | { 14 | /// Initializes a new instance of the class. 15 | public UserAccountException() : base("A User Account Exception has occurred.") 16 | { 17 | 18 | } 19 | 20 | /// 21 | /// Initializes a new instance of the class with a specified error message. 22 | /// 23 | /// The message that describes the error. 24 | public UserAccountException(string? message) : base(message) 25 | { 26 | 27 | } 28 | 29 | /// 30 | /// Initializes a new instance of the class with a specified error message 31 | /// and a reference to the inner exception that is the cause of this exception. 32 | /// 33 | /// The error message that explains the reason for the exception. 34 | /// 35 | /// The exception that is the cause of the current exception, or a null reference ( 36 | /// in Visual Basic) if no inner exception is specified. 37 | /// 38 | public UserAccountException(string? message, Exception? innerException) : base(message, innerException) 39 | { 40 | 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /QuickApp.Core/Services/Account/Exceptions/UserNotFoundException.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Services.Account 8 | { 9 | /// 10 | /// The exception that is thrown when an attempt to access a particular User Account fails. 11 | /// 12 | public class UserNotFoundException : UserAccountException 13 | { 14 | /// Initializes a new instance of the class. 15 | public UserNotFoundException() : base("Unable to find the requested User.") 16 | { 17 | 18 | } 19 | 20 | /// 21 | /// Initializes a new instance of the class with a specified error message. 22 | /// 23 | /// The message that describes the error. 24 | public UserNotFoundException(string? message) : base(message) 25 | { 26 | 27 | } 28 | 29 | /// 30 | /// Initializes a new instance of the class with a specified error message 31 | /// and a reference to the inner exception that is the cause of this exception. 32 | /// 33 | /// The error message that explains the reason for the exception. 34 | /// 35 | /// The exception that is the cause of the current exception, or a null reference ( 36 | /// in Visual Basic) if no inner exception is specified. 37 | /// 38 | public UserNotFoundException(string? message, Exception? innerException) : base(message, innerException) 39 | { 40 | 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /QuickApp.Core/Services/Account/Exceptions/UserRoleException.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Services.Account 8 | { 9 | /// 10 | /// Represents errors that occur with user role related operations. 11 | /// 12 | public class UserRoleException : Exception 13 | { 14 | /// Initializes a new instance of the class. 15 | public UserRoleException() : base("A User Role Exception has occurred.") 16 | { 17 | 18 | } 19 | 20 | /// 21 | /// Initializes a new instance of the class with a specified error message. 22 | /// 23 | /// The message that describes the error. 24 | public UserRoleException(string? message) : base(message) 25 | { 26 | 27 | } 28 | 29 | /// 30 | /// Initializes a new instance of the class with a specified error message 31 | /// and a reference to the inner exception that is the cause of this exception. 32 | /// 33 | /// The error message that explains the reason for the exception. 34 | /// 35 | /// The exception that is the cause of the current exception, or a null reference ( 36 | /// in Visual Basic) if no inner exception is specified. 37 | /// 38 | public UserRoleException(string? message, Exception? innerException) : base(message, innerException) 39 | { 40 | 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /QuickApp.Core/Services/Account/Interfaces/IUserAccountService.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using QuickApp.Core.Models.Account; 8 | 9 | namespace QuickApp.Core.Services.Account 10 | { 11 | public interface IUserAccountService 12 | { 13 | Task CheckPasswordAsync(ApplicationUser user, string password); 14 | Task<(bool Succeeded, string[] Errors)> CreateUserAsync(ApplicationUser user, IEnumerable roles, string password); 15 | Task<(bool Succeeded, string[] Errors)> DeleteUserAsync(ApplicationUser user); 16 | Task<(bool Succeeded, string[] Errors)> DeleteUserAsync(string userId); 17 | Task<(ApplicationUser User, string[] Roles)?> GetUserAndRolesAsync(string userId); 18 | Task GetUserByEmailAsync(string email); 19 | Task GetUserByIdAsync(string userId); 20 | Task GetUserByUserNameAsync(string userName); 21 | Task> GetUserRolesAsync(ApplicationUser user); 22 | Task> GetUsersAndRolesAsync(int page, int pageSize); 23 | Task<(bool Succeeded, string[] Errors)> ResetPasswordAsync(ApplicationUser user, string newPassword); 24 | Task<(bool Success, string[] Errors)> TestCanDeleteUserAsync(string userId); 25 | Task<(bool Succeeded, string[] Errors)> UpdatePasswordAsync(ApplicationUser user, string currentPassword, string newPassword); 26 | Task<(bool Succeeded, string[] Errors)> UpdateUserAsync(ApplicationUser user); 27 | Task<(bool Succeeded, string[] Errors)> UpdateUserAsync(ApplicationUser user, IEnumerable? roles); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /QuickApp.Core/Services/Account/Interfaces/IUserIdAccessor.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Services.Account 8 | { 9 | public interface IUserIdAccessor 10 | { 11 | string? GetCurrentUserId(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /QuickApp.Core/Services/Account/Interfaces/IUserRoleService.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using QuickApp.Core.Models.Account; 8 | 9 | namespace QuickApp.Core.Services.Account 10 | { 11 | public interface IUserRoleService 12 | { 13 | Task<(bool Succeeded, string[] Errors)> CreateRoleAsync(ApplicationRole role, IEnumerable claims); 14 | Task<(bool Succeeded, string[] Errors)> DeleteRoleAsync(ApplicationRole role); 15 | Task<(bool Succeeded, string[] Errors)> DeleteRoleAsync(string roleName); 16 | Task GetRoleByIdAsync(string roleId); 17 | Task GetRoleByNameAsync(string roleName); 18 | Task GetRoleLoadRelatedAsync(string roleName); 19 | Task> GetRolesLoadRelatedAsync(int page, int pageSize); 20 | Task<(bool Success, string[] Errors)> TestCanDeleteRoleAsync(string roleId); 21 | Task<(bool Succeeded, string[] Errors)> UpdateRoleAsync(ApplicationRole role, IEnumerable? claims); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /QuickApp.Core/Services/IEmailSender.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Services 8 | { 9 | public interface IEmailSender 10 | { 11 | Task<(bool success, string? errorMsg)> SendEmailAsync( 12 | string recipientName, 13 | string recipientEmail, 14 | string subject, 15 | string body, 16 | bool isHtml = true); 17 | 18 | Task<(bool success, string? errorMsg)> SendEmailAsync( 19 | string senderName, 20 | string senderEmail, 21 | string recipientName, 22 | string recipientEmail, 23 | string subject, 24 | string body, 25 | bool isHtml = true); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /QuickApp.Core/Services/Shop/CustomerService.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using Microsoft.EntityFrameworkCore; 8 | using QuickApp.Core.Infrastructure; 9 | using QuickApp.Core.Models.Shop; 10 | 11 | namespace QuickApp.Core.Services.Shop 12 | { 13 | public class CustomerService(ApplicationDbContext dbContext) : ICustomerService 14 | { 15 | public IEnumerable GetTopActiveCustomers(int count) => throw new NotImplementedException(); 16 | 17 | public IEnumerable GetAllCustomersData() => dbContext.Customers 18 | .Include(c => c.Orders).ThenInclude(o => o.OrderDetails).ThenInclude(d => d.Product) 19 | .Include(c => c.Orders).ThenInclude(o => o.Cashier) 20 | .AsSingleQuery() 21 | .OrderBy(c => c.Name) 22 | .ToList(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /QuickApp.Core/Services/Shop/Exceptions/CustomerException.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Services.Shop 8 | { 9 | /// 10 | /// Represents errors that occur with customer related operations. 11 | /// 12 | public class CustomerException : Exception 13 | { 14 | /// Initializes a new instance of the class. 15 | public CustomerException() : base("A Customer Exception has occurred.") 16 | { 17 | 18 | } 19 | 20 | /// 21 | /// Initializes a new instance of the class with a specified error message. 22 | /// 23 | /// The message that describes the error. 24 | public CustomerException(string? message) : base(message) 25 | { 26 | 27 | } 28 | 29 | /// 30 | /// Initializes a new instance of the class with a specified error message 31 | /// and a reference to the inner exception that is the cause of this exception. 32 | /// 33 | /// The error message that explains the reason for the exception. 34 | /// 35 | /// The exception that is the cause of the current exception, or a null reference ( 36 | /// in Visual Basic) if no inner exception is specified. 37 | /// 38 | public CustomerException(string? message, Exception? innerException) : base(message, innerException) 39 | { 40 | 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /QuickApp.Core/Services/Shop/Interfaces/ICustomerService.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using QuickApp.Core.Models.Shop; 8 | 9 | namespace QuickApp.Core.Services.Shop 10 | { 11 | public interface ICustomerService 12 | { 13 | IEnumerable GetTopActiveCustomers(int count); 14 | IEnumerable GetAllCustomersData(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /QuickApp.Core/Services/Shop/Interfaces/IOrdersService.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Services.Shop 8 | { 9 | public interface IOrdersService 10 | { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /QuickApp.Core/Services/Shop/Interfaces/IProductService.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Services.Shop 8 | { 9 | public interface IProductService 10 | { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /QuickApp.Core/Services/Shop/OrdersService.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Services.Shop 8 | { 9 | public class OrdersService() : IOrdersService 10 | { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /QuickApp.Core/Services/Shop/ProductService.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Core.Services.Shop 8 | { 9 | public class ProductService() : IProductService 10 | { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /QuickApp.Server/Attributes/MinimumCountAttribute.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using System.Collections; 8 | using System.ComponentModel.DataAnnotations; 9 | 10 | namespace QuickApp.Server.Attributes 11 | { 12 | [AttributeUsage(AttributeTargets.Property)] 13 | public sealed class MinimumCountAttribute(int minCount, bool required = true, bool allowEmptyStringValues = false) : 14 | ValidationAttribute("'{0}' must have at least {1} item.") 15 | { 16 | public MinimumCountAttribute() : this(1) 17 | { 18 | 19 | } 20 | 21 | public override bool IsValid(object? value) 22 | { 23 | if (value == null) 24 | return !required; 25 | 26 | if (!allowEmptyStringValues && value is ICollection stringList) 27 | return stringList.Count(s => !string.IsNullOrWhiteSpace(s)) >= minCount; 28 | 29 | if (value is ICollection list) 30 | return list.Count >= minCount; 31 | 32 | return false; 33 | } 34 | 35 | public override string FormatErrorMessage(string name) 36 | { 37 | return string.Format(ErrorMessageString, name, minCount); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /QuickApp.Server/Attributes/SanitizeModelAttribute.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using Microsoft.AspNetCore.Mvc.Filters; 8 | 9 | namespace QuickApp.Server.Attributes 10 | { 11 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 12 | public class SanitizeModelAttribute : ActionFilterAttribute 13 | { 14 | public override void OnActionExecuting(ActionExecutingContext context) 15 | { 16 | foreach (var arg in context.ActionArguments.Values) 17 | { 18 | if (arg is ISanitizeModel model) 19 | { 20 | model.SanitizeModel(); 21 | } 22 | } 23 | } 24 | } 25 | 26 | public interface ISanitizeModel 27 | { 28 | public void SanitizeModel(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /QuickApp.Server/Authorization/AuthPolicies.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Server.Authorization 8 | { 9 | public static class AuthPolicies 10 | { 11 | ///Policy to allow viewing all user records. 12 | public const string ViewAllUsersPolicy = "View All Users"; 13 | 14 | ///Policy to allow adding, removing and updating all user records. 15 | public const string ManageAllUsersPolicy = "Manage All Users"; 16 | 17 | /// Policy to allow viewing details of all roles. 18 | public const string ViewAllRolesPolicy = "View All Roles"; 19 | 20 | /// Policy to allow viewing details of all or specific roles (Requires roleName as parameter). 21 | public const string ViewRoleByRoleNamePolicy = "View Role by RoleName"; 22 | 23 | /// Policy to allow adding, removing and updating all roles. 24 | public const string ManageAllRolesPolicy = "Manage All Roles"; 25 | 26 | /// Policy to allow assigning roles the user has access to (Requires new and current roles as parameter). 27 | public const string AssignAllowedRolesPolicy = "Assign Allowed Roles"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /QuickApp.Server/Authorization/Requirements/AssignRolesAuthorizationRequirement.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using Microsoft.AspNetCore.Authorization; 8 | using QuickApp.Core.Services.Account; 9 | using System.Security.Claims; 10 | 11 | namespace QuickApp.Server.Authorization.Requirements 12 | { 13 | public class AssignRolesAuthorizationRequirement : IAuthorizationRequirement 14 | { 15 | 16 | } 17 | 18 | public class AssignRolesAuthorizationHandler : 19 | AuthorizationHandler 20 | { 21 | protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, 22 | AssignRolesAuthorizationRequirement requirement, (string[] newRoles, string[] currentRoles) roles) 23 | { 24 | if (!GetIsRolesChanged(roles.newRoles, roles.currentRoles)) 25 | { 26 | context.Succeed(requirement); 27 | } 28 | else if (context.User.HasClaim(CustomClaims.Permission, ApplicationPermissions.AssignRoles)) 29 | { 30 | // If user has ViewRoles permission, then he can assign any roles 31 | if (context.User.HasClaim(CustomClaims.Permission, ApplicationPermissions.ViewRoles)) 32 | context.Succeed(requirement); 33 | 34 | // Else user can only assign roles they're part of 35 | else if (GetIsUserInAllAddedRoles(context.User, roles.newRoles, roles.currentRoles)) 36 | context.Succeed(requirement); 37 | } 38 | 39 | return Task.CompletedTask; 40 | } 41 | 42 | private static bool GetIsRolesChanged(string[] newRoles, string[] currentRoles) 43 | { 44 | newRoles ??= []; 45 | currentRoles ??= []; 46 | 47 | var roleAdded = newRoles.Except(currentRoles).Any(); 48 | var roleRemoved = currentRoles.Except(newRoles).Any(); 49 | 50 | return roleAdded || roleRemoved; 51 | } 52 | 53 | private static bool GetIsUserInAllAddedRoles(ClaimsPrincipal contextUser, string[] newRoles, string[] currentRoles) 54 | { 55 | newRoles ??= []; 56 | currentRoles ??= []; 57 | 58 | var addedRoles = newRoles.Except(currentRoles); 59 | 60 | return addedRoles.All(contextUser.IsInRole); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /QuickApp.Server/Authorization/Requirements/UserAccountAuthorizationRequirement.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using Microsoft.AspNetCore.Authorization; 8 | using QuickApp.Core.Services.Account; 9 | using QuickApp.Server.Services; 10 | using System.Security.Claims; 11 | 12 | namespace QuickApp.Server.Authorization.Requirements 13 | { 14 | public class UserAccountAuthorizationRequirement(string operationName) : IAuthorizationRequirement 15 | { 16 | public string OperationName { get; private set; } = operationName; 17 | } 18 | 19 | public class ViewUserAuthorizationHandler : AuthorizationHandler 20 | { 21 | protected override Task HandleRequirementAsync( 22 | AuthorizationHandlerContext context, UserAccountAuthorizationRequirement requirement, string targetUserId) 23 | { 24 | if (context.User == null || requirement.OperationName != UserAccountManagementOperations.ReadOperationName) 25 | return Task.CompletedTask; 26 | 27 | if (context.User.HasClaim(CustomClaims.Permission, ApplicationPermissions.ViewUsers) 28 | || GetIsSameUser(context.User, targetUserId)) 29 | context.Succeed(requirement); 30 | 31 | return Task.CompletedTask; 32 | } 33 | 34 | private static bool GetIsSameUser(ClaimsPrincipal user, string targetUserId) 35 | { 36 | if (string.IsNullOrWhiteSpace(targetUserId)) 37 | return false; 38 | 39 | return Utilities.GetUserId(user) == targetUserId; 40 | } 41 | } 42 | 43 | public class ManageUserAuthorizationHandler : AuthorizationHandler 44 | { 45 | protected override Task HandleRequirementAsync( 46 | AuthorizationHandlerContext context, UserAccountAuthorizationRequirement requirement, string targetUserId) 47 | { 48 | if (context.User == null || 49 | (requirement.OperationName != UserAccountManagementOperations.CreateOperationName && 50 | requirement.OperationName != UserAccountManagementOperations.UpdateOperationName && 51 | requirement.OperationName != UserAccountManagementOperations.DeleteOperationName)) 52 | return Task.CompletedTask; 53 | 54 | if (context.User.HasClaim(CustomClaims.Permission, ApplicationPermissions.ManageUsers) 55 | || GetIsSameUser(context.User, targetUserId)) 56 | context.Succeed(requirement); 57 | 58 | return Task.CompletedTask; 59 | } 60 | 61 | private static bool GetIsSameUser(ClaimsPrincipal user, string targetUserId) 62 | { 63 | if (string.IsNullOrWhiteSpace(targetUserId)) 64 | return false; 65 | 66 | return Utilities.GetUserId(user) == targetUserId; 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /QuickApp.Server/Authorization/Requirements/ViewRoleAuthorizationRequirement.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using Microsoft.AspNetCore.Authorization; 8 | using QuickApp.Core.Services.Account; 9 | 10 | namespace QuickApp.Server.Authorization.Requirements 11 | { 12 | public class ViewRoleAuthorizationRequirement : IAuthorizationRequirement 13 | { 14 | 15 | } 16 | 17 | public class ViewRoleAuthorizationHandler : AuthorizationHandler 18 | { 19 | protected override Task HandleRequirementAsync( 20 | AuthorizationHandlerContext context, ViewRoleAuthorizationRequirement requirement, string roleName) 21 | { 22 | if (context.User == null) 23 | return Task.CompletedTask; 24 | 25 | if (context.User.HasClaim(CustomClaims.Permission, ApplicationPermissions.ViewRoles) 26 | || context.User.IsInRole(roleName)) 27 | context.Succeed(requirement); 28 | 29 | return Task.CompletedTask; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /QuickApp.Server/Authorization/SwaggerAuthorizeOperationFilter.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using Microsoft.AspNetCore.Authorization; 8 | using Microsoft.OpenApi.Models; 9 | using Swashbuckle.AspNetCore.SwaggerGen; 10 | 11 | namespace QuickApp.Server.Authorization 12 | { 13 | // Swagger IOperationFilter implementation that will decide which api action needs authorization 14 | internal class SwaggerAuthorizeOperationFilter : IOperationFilter 15 | { 16 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 17 | { 18 | // Check for authorize attribute 19 | var hasAuthorize = context.MethodInfo.DeclaringType?.GetCustomAttributes(true) 20 | .Union(context.MethodInfo.GetCustomAttributes(true)) 21 | .OfType() 22 | .Any(); 23 | 24 | if (hasAuthorize == true) 25 | { 26 | operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" }); 27 | 28 | var oAuthScheme = new OpenApiSecurityScheme 29 | { 30 | Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" } 31 | }; 32 | 33 | operation.Security = 34 | [ 35 | new() { [oAuthScheme] = [] } 36 | ]; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /QuickApp.Server/Authorization/UserAccountManagementOperations.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using QuickApp.Server.Authorization.Requirements; 8 | 9 | namespace QuickApp.Server.Authorization 10 | { 11 | /// 12 | /// Operation Policy to allow adding, viewing, updating and deleting general or specific user records. 13 | /// 14 | public static class UserAccountManagementOperations 15 | { 16 | public const string CreateOperationName = "Create"; 17 | public const string ReadOperationName = "Read"; 18 | public const string UpdateOperationName = "Update"; 19 | public const string DeleteOperationName = "Delete"; 20 | 21 | public static readonly UserAccountAuthorizationRequirement CreateOperationRequirement = new(CreateOperationName); 22 | public static readonly UserAccountAuthorizationRequirement ReadOperationRequirement = new(ReadOperationName); 23 | public static readonly UserAccountAuthorizationRequirement UpdateOperationRequirement = new(UpdateOperationName); 24 | public static readonly UserAccountAuthorizationRequirement DeleteOperationRequirement = new(DeleteOperationName); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /QuickApp.Server/Configuration/AppSettings.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Server.Configuration 8 | { 9 | public class AppSettings 10 | { 11 | public SmtpConfig? SmtpConfig { get; set; } 12 | } 13 | 14 | public class SmtpConfig 15 | { 16 | public required string Host { get; set; } 17 | public int Port { get; set; } 18 | public bool UseSSL { get; set; } 19 | 20 | public required string EmailAddress { get; set; } 21 | public string? Name { get; set; } 22 | public string? Username { get; set; } 23 | public string? Password { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /QuickApp.Server/Configuration/DesignTimeDbContextFactory.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.EntityFrameworkCore.Design; 9 | using QuickApp.Core.Infrastructure; 10 | using QuickApp.Server.Services; 11 | using System.Reflection; 12 | 13 | namespace QuickApp.Server.Configuration 14 | { 15 | public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory 16 | { 17 | public ApplicationDbContext CreateDbContext(string[] args) 18 | { 19 | var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development"; 20 | 21 | var configuration = new ConfigurationBuilder() 22 | .SetBasePath(Directory.GetCurrentDirectory()) 23 | .AddJsonFile("appsettings.json") 24 | .AddJsonFile($"appsettings.{env}.json", optional: true) 25 | .Build(); 26 | 27 | var builder = new DbContextOptionsBuilder(); 28 | var migrationsAssembly = typeof(Program).GetTypeInfo().Assembly.GetName().Name; 29 | 30 | builder.UseSqlServer(configuration["ConnectionStrings:DefaultConnection"], b => b.MigrationsAssembly(migrationsAssembly)); 31 | builder.UseOpenIddict(); 32 | 33 | return new ApplicationDbContext(builder.Options, SystemUserIdAccessor.GetNewAccessor()); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /QuickApp.Server/Configuration/MappingProfile.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using AutoMapper; 8 | using Microsoft.AspNetCore.Identity; 9 | using QuickApp.Core.Models.Account; 10 | using QuickApp.Core.Models.Shop; 11 | using QuickApp.Core.Services.Account; 12 | using QuickApp.Server.ViewModels.Account; 13 | using QuickApp.Server.ViewModels.Shop; 14 | 15 | namespace QuickApp.Server.Configuration 16 | { 17 | public class MappingProfile : Profile 18 | { 19 | public MappingProfile() 20 | { 21 | CreateMap() 22 | .ForMember(d => d.Roles, map => map.Ignore()); 23 | CreateMap() 24 | .ForMember(d => d.Roles, map => map.Ignore()) 25 | .ForMember(d => d.Id, map => map.Condition(src => src.Id != null)); 26 | 27 | CreateMap() 28 | .ForMember(d => d.Roles, map => map.Ignore()); 29 | CreateMap() 30 | .ForMember(d => d.Roles, map => map.Ignore()) 31 | .ForMember(d => d.Id, map => map.Condition(src => src.Id != null)); 32 | 33 | CreateMap() 34 | .ReverseMap(); 35 | 36 | CreateMap() 37 | .ForMember(d => d.Permissions, map => map.MapFrom(s => s.Claims)) 38 | .ForMember(d => d.UsersCount, map => map.MapFrom(s => s.Users != null ? s.Users.Count : 0)) 39 | .ReverseMap(); 40 | CreateMap() 41 | .ForMember(d => d.Id, map => map.Condition(src => src.Id != null)); 42 | 43 | CreateMap, ClaimVM>() 44 | .ForMember(d => d.Type, map => map.MapFrom(s => s.ClaimType)) 45 | .ForMember(d => d.Value, map => map.MapFrom(s => s.ClaimValue)) 46 | .ReverseMap(); 47 | 48 | CreateMap() 49 | .ReverseMap(); 50 | 51 | CreateMap, PermissionVM>() 52 | .ConvertUsing(s => ((PermissionVM)ApplicationPermissions.GetPermissionByValue(s.ClaimValue))!); 53 | 54 | CreateMap() 55 | .ReverseMap(); 56 | 57 | CreateMap() 58 | .ReverseMap(); 59 | 60 | CreateMap() 61 | .ReverseMap(); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /QuickApp.Server/Configuration/OidcServerConfig.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using OpenIddict.Abstractions; 8 | using static OpenIddict.Abstractions.OpenIddictConstants; 9 | 10 | namespace QuickApp.Server.Configuration 11 | { 12 | public static class OidcServerConfig 13 | { 14 | public const string ServerName = "QuickApp API"; 15 | public const string QuickAppClientID = "quickapp_spa"; 16 | public const string SwaggerClientID = "swagger_ui"; 17 | 18 | public static async Task RegisterClientApplicationsAsync(IServiceProvider provider) 19 | { 20 | var manager = provider.GetRequiredService(); 21 | 22 | // Angular SPA Client 23 | if (await manager.FindByClientIdAsync(QuickAppClientID) is null) 24 | { 25 | await manager.CreateAsync(new OpenIddictApplicationDescriptor 26 | { 27 | ClientId = QuickAppClientID, 28 | ClientType = ClientTypes.Public, 29 | DisplayName = "QuickApp SPA", 30 | Permissions = 31 | { 32 | Permissions.Endpoints.Token, 33 | Permissions.GrantTypes.Password, 34 | Permissions.GrantTypes.RefreshToken, 35 | Permissions.Scopes.Profile, 36 | Permissions.Scopes.Email, 37 | Permissions.Scopes.Phone, 38 | Permissions.Scopes.Address, 39 | Permissions.Scopes.Roles 40 | } 41 | }); 42 | } 43 | 44 | // Swagger UI Client 45 | if (await manager.FindByClientIdAsync(SwaggerClientID) is null) 46 | { 47 | await manager.CreateAsync(new OpenIddictApplicationDescriptor 48 | { 49 | ClientId = SwaggerClientID, 50 | ClientType = ClientTypes.Public, 51 | DisplayName = "Swagger UI", 52 | Permissions = 53 | { 54 | Permissions.Endpoints.Token, 55 | Permissions.GrantTypes.Password 56 | } 57 | }); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /QuickApp.Server/Controllers/BaseApiController.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using AutoMapper; 8 | using Microsoft.AspNetCore.Mvc; 9 | using QuickApp.Core.Services.Account; 10 | using QuickApp.Server.Attributes; 11 | using QuickApp.Server.Services; 12 | 13 | namespace QuickApp.Server.Controllers 14 | { 15 | [Route("api/[controller]")] 16 | [ApiController] 17 | [SanitizeModel] 18 | public class BaseApiController : ControllerBase 19 | { 20 | protected readonly IMapper _mapper; 21 | protected readonly ILogger _logger; 22 | 23 | public BaseApiController(ILogger logger, IMapper mapper) 24 | { 25 | _logger = logger; 26 | _mapper = mapper; 27 | } 28 | 29 | protected string GetCurrentUserId(string errorMsg = "Error retrieving the userId for the current user.") 30 | { 31 | return Utilities.GetUserId(User) ?? throw new UserNotFoundException(errorMsg); 32 | } 33 | 34 | protected void AddModelError(IEnumerable errors, string key = "") 35 | { 36 | foreach (var error in errors) 37 | { 38 | AddModelError(error, key); 39 | } 40 | } 41 | 42 | protected void AddModelError(string error, string key = "") 43 | { 44 | ModelState.AddModelError(key, error); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /QuickApp.Server/Controllers/CustomerController.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using AutoMapper; 8 | using Microsoft.AspNetCore.Mvc; 9 | using QuickApp.Core.Services; 10 | using QuickApp.Core.Services.Shop; 11 | using QuickApp.Server.Services.Email; 12 | using QuickApp.Server.ViewModels.Shop; 13 | 14 | namespace QuickApp.Server.Controllers 15 | { 16 | [Route("api/[controller]")] 17 | [ApiController] 18 | public class CustomerController : ControllerBase 19 | { 20 | private readonly IMapper _mapper; 21 | private readonly ILogger _logger; 22 | private readonly IEmailSender _emailSender; 23 | private readonly ICustomerService _customerService; 24 | 25 | public CustomerController(IMapper mapper, ILogger logger, IEmailSender emailSender, 26 | ICustomerService customerService) 27 | { 28 | _mapper = mapper; 29 | _logger = logger; 30 | _emailSender = emailSender; 31 | _customerService = customerService; 32 | } 33 | 34 | [HttpGet] 35 | public IActionResult Get() 36 | { 37 | var allCustomers = _customerService.GetAllCustomersData(); 38 | return Ok(_mapper.Map>(allCustomers)); 39 | } 40 | 41 | [HttpGet("throw")] 42 | public IEnumerable Throw() 43 | { 44 | throw new CustomerException($"This is a test exception: {DateTime.Now}"); 45 | } 46 | 47 | [HttpGet("email")] 48 | public async Task Email() 49 | { 50 | var recipientName = "QickApp Tester"; // <===== Put the recipient's name here 51 | var recipientEmail = "test@ebenmonney.com"; // <===== Put the recipient's email here 52 | 53 | var message = EmailTemplates.GetTestEmail(recipientName, DateTime.UtcNow); 54 | 55 | (var success, var errorMsg) = await _emailSender.SendEmailAsync(recipientName, recipientEmail, 56 | "Test Email from QuickApp", message); 57 | 58 | if (success) 59 | return "Success"; 60 | 61 | return $"Error: {errorMsg}"; 62 | } 63 | 64 | [HttpGet("{id}")] 65 | public string Get(int id) 66 | { 67 | return $"value: {id}"; 68 | } 69 | 70 | [HttpPost] 71 | public void Post([FromBody] string value) 72 | { 73 | } 74 | 75 | [HttpPut("{id}")] 76 | public void Put(int id, [FromBody] string value) 77 | { 78 | } 79 | 80 | [HttpDelete("{id}")] 81 | public void Delete(int id) 82 | { 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /QuickApp.Server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "http": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "applicationUrl": "http://localhost:5225", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development", 11 | "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" 12 | } 13 | }, 14 | "https": { 15 | "commandName": "Project", 16 | "dotnetRunMessages": true, 17 | "launchBrowser": true, 18 | "applicationUrl": "https://localhost:7085;http://localhost:5225", 19 | "environmentVariables": { 20 | "ASPNETCORE_ENVIRONMENT": "Development", 21 | "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" 22 | } 23 | } 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /QuickApp.Server/QuickApp.Server.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | ..\quickapp.client 8 | npm start 9 | https://localhost:4200 10 | 9.19.0 11 | www.ebenmonney.com 12 | $(Company) 13 | ASPNET Core 9.0 - Angular 19 startup template for Quick Application Development 14 | Copyright © 2024 www.ebenmonney.com 15 | https://www.ebenmonney.com/quickapp 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | all 33 | runtime; build; native; contentfiles; analyzers; buildtransitive 34 | 35 | 36 | 9.*-* 37 | 38 | 39 | 40 | 41 | 42 | false 43 | 44 | 45 | 46 | 47 | 48 | 49 | PreserveNewest 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /QuickApp.Server/Services/Email/EmailTemplates.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Server.Services.Email 8 | { 9 | public static class EmailTemplates 10 | { 11 | private static IWebHostEnvironment? _hostingEnvironment; 12 | private static string? testEmailTemplate; 13 | private static string? plainTextTestEmailTemplate; 14 | 15 | public static void Initialize(IWebHostEnvironment hostingEnvironment) 16 | { 17 | _hostingEnvironment = hostingEnvironment; 18 | } 19 | 20 | public static string GetTestEmail(string recipientName, DateTime testDate) 21 | { 22 | testEmailTemplate ??= ReadPhysicalFile("Services/Email/TestEmail.template"); 23 | 24 | var emailMessage = testEmailTemplate 25 | .Replace("{user}", recipientName) 26 | .Replace("{testDate}", testDate.ToString()); 27 | 28 | return emailMessage; 29 | } 30 | 31 | public static string GetPlainTextTestEmail(DateTime date) 32 | { 33 | plainTextTestEmailTemplate ??= ReadPhysicalFile("Services/Email/PlainTextTestEmail.template"); 34 | 35 | var emailMessage = plainTextTestEmailTemplate 36 | .Replace("{date}", date.ToString()); 37 | 38 | return emailMessage; 39 | } 40 | 41 | private static string ReadPhysicalFile(string path) 42 | { 43 | if (_hostingEnvironment == null) 44 | throw new InvalidOperationException($"{nameof(EmailTemplates)} is not initialized"); 45 | 46 | var fileInfo = _hostingEnvironment.ContentRootFileProvider.GetFileInfo(path); 47 | 48 | if (!fileInfo.Exists) 49 | throw new FileNotFoundException($"Template file located at \"{path}\" was not found"); 50 | 51 | using var fs = fileInfo.CreateReadStream(); 52 | using var sr = new StreamReader(fs); 53 | return sr.ReadToEnd(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /QuickApp.Server/Services/Email/PlainTextTestEmail.template: -------------------------------------------------------------------------------- 1 | Hello, 2 | This email was sent using the plain text test email template. 3 | The test was on {date}. 4 | 5 | Regards, 6 | QuickApp Template -------------------------------------------------------------------------------- /QuickApp.Server/Services/Email/TestEmail.template: -------------------------------------------------------------------------------- 1 | 
2 |

Hello {user},

3 |

4 | This is a TEST email. 5 |

6 |

The request was on {testDate}.

7 |

8 |
9 |

10 |

Regards,

11 |

QuickApp Template

12 |
-------------------------------------------------------------------------------- /QuickApp.Server/Services/UserIdAccessor.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using QuickApp.Core.Services.Account; 8 | using System.Security.Claims; 9 | using static OpenIddict.Abstractions.OpenIddictConstants; 10 | 11 | namespace QuickApp.Server.Services 12 | { 13 | public class UserIdAccessor(IHttpContextAccessor httpContextAccessor) : IUserIdAccessor 14 | { 15 | private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; 16 | 17 | public string? GetCurrentUserId() => _httpContextAccessor.HttpContext?.User.FindFirstValue(Claims.Subject); 18 | } 19 | 20 | public class SystemUserIdAccessor : IUserIdAccessor 21 | { 22 | private readonly string? id; 23 | 24 | private SystemUserIdAccessor(string? id) => this.id = id; 25 | 26 | public string? GetCurrentUserId() => id; 27 | 28 | public static SystemUserIdAccessor GetNewAccessor(string? id = "SYSTEM") => new(id); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /QuickApp.Server/Services/Utilities.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using System.Security.Claims; 8 | using static OpenIddict.Abstractions.OpenIddictConstants; 9 | 10 | namespace QuickApp.Server.Services 11 | { 12 | public static class Utilities 13 | { 14 | public static void QuickLog(string text, string logPath) 15 | { 16 | var dirPath = Path.GetDirectoryName(logPath); 17 | 18 | if (string.IsNullOrWhiteSpace(dirPath)) 19 | throw new ArgumentException($"Specified path \"{logPath}\" is invalid", nameof(logPath)); 20 | 21 | if (!Directory.Exists(dirPath)) 22 | Directory.CreateDirectory(dirPath); 23 | 24 | using var writer = File.AppendText(logPath); 25 | writer.WriteLine($"{DateTime.Now} - {text}"); 26 | } 27 | 28 | public static string? GetUserId(ClaimsPrincipal user) 29 | { 30 | return user.FindFirstValue(Claims.Subject)?.Trim(); 31 | } 32 | 33 | public static string[] GetRoles(ClaimsPrincipal user) 34 | { 35 | return user.Claims 36 | .Where(c => c.Type == Claims.Role) 37 | .Select(c => c.Value) 38 | .ToArray(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /QuickApp.Server/ViewModels/Account/ClaimVM.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Server.ViewModels.Account 8 | { 9 | public class ClaimVM 10 | { 11 | public string? Type { get; set; } 12 | public string? Value { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /QuickApp.Server/ViewModels/Account/PermissionVM.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using QuickApp.Core.Models.Account; 8 | using System.Diagnostics.CodeAnalysis; 9 | 10 | namespace QuickApp.Server.ViewModels.Account 11 | { 12 | public class PermissionVM 13 | { 14 | public string? Name { get; set; } 15 | public string? Value { get; set; } 16 | public string? GroupName { get; set; } 17 | public string? Description { get; set; } 18 | 19 | [return: NotNullIfNotNull(nameof(permission))] 20 | public static explicit operator PermissionVM?(ApplicationPermission? permission) 21 | { 22 | if (permission == null) 23 | return null; 24 | 25 | return new PermissionVM 26 | { 27 | Name = permission.Name, 28 | Value = permission.Value, 29 | GroupName = permission.GroupName, 30 | Description = permission.Description 31 | }; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /QuickApp.Server/ViewModels/Account/RoleVM.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using QuickApp.Core.Extensions; 8 | using QuickApp.Server.Attributes; 9 | using System.ComponentModel.DataAnnotations; 10 | 11 | namespace QuickApp.Server.ViewModels.Account 12 | { 13 | public class RoleVM : ISanitizeModel 14 | { 15 | public virtual void SanitizeModel() 16 | { 17 | Id = Id.NullIfWhiteSpace(); 18 | Name = Name.NullIfWhiteSpace(); 19 | Description = Description.NullIfWhiteSpace(); 20 | } 21 | 22 | public string? Id { get; set; } 23 | 24 | [Required(ErrorMessage = "Role name is required"), 25 | StringLength(200, MinimumLength = 2, ErrorMessage = "Role name must be between 2 and 200 characters")] 26 | public string? Name { get; set; } 27 | 28 | public string? Description { get; set; } 29 | 30 | public int UsersCount { get; set; } 31 | 32 | public PermissionVM[]? Permissions { get; set; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /QuickApp.Server/ViewModels/Account/UserVMs.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using QuickApp.Core.Extensions; 8 | using QuickApp.Server.Attributes; 9 | using System.ComponentModel.DataAnnotations; 10 | 11 | namespace QuickApp.Server.ViewModels.Account 12 | { 13 | public class UserVM : UserBaseVM 14 | { 15 | public bool IsLockedOut { get; set; } 16 | 17 | [MinimumCount(1, ErrorMessage = "Roles cannot be empty")] 18 | public string[]? Roles { get; set; } 19 | } 20 | 21 | public class UserEditVM : UserBaseVM 22 | { 23 | public string? CurrentPassword { get; set; } 24 | 25 | [MinLength(6, ErrorMessage = "New Password must be at least 6 characters")] 26 | public string? NewPassword { get; set; } 27 | 28 | [MinimumCount(1, false, ErrorMessage = "Roles cannot be empty")] 29 | public string[]? Roles { get; set; } 30 | } 31 | 32 | public class UserPatchVM 33 | { 34 | public string? FullName { get; set; } 35 | 36 | public string? JobTitle { get; set; } 37 | 38 | public string? PhoneNumber { get; set; } 39 | 40 | public string? Configuration { get; set; } 41 | } 42 | 43 | public abstract class UserBaseVM : ISanitizeModel 44 | { 45 | public virtual void SanitizeModel() 46 | { 47 | Id = Id.NullIfWhiteSpace(); 48 | FullName = FullName.NullIfWhiteSpace(); 49 | JobTitle = JobTitle.NullIfWhiteSpace(); 50 | PhoneNumber = PhoneNumber.NullIfWhiteSpace(); 51 | Configuration = Configuration.NullIfWhiteSpace(); 52 | } 53 | 54 | public string? Id { get; set; } 55 | 56 | [Required(ErrorMessage = "Username is required"), 57 | StringLength(200, MinimumLength = 2, ErrorMessage = "Username must be between 2 and 200 characters")] 58 | public required string UserName { get; set; } 59 | 60 | public string? FullName { get; set; } 61 | 62 | [Required(ErrorMessage = "Email is required"), 63 | StringLength(200, ErrorMessage = "Email must be at most 200 characters"), 64 | EmailAddress(ErrorMessage = "Invalid email address")] 65 | public required string Email { get; set; } 66 | 67 | public string? JobTitle { get; set; } 68 | 69 | public string? PhoneNumber { get; set; } 70 | 71 | public string? Configuration { get; set; } 72 | 73 | public bool IsEnabled { get; set; } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /QuickApp.Server/ViewModels/Shop/CustomerVM.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | using FluentValidation; 8 | 9 | namespace QuickApp.Server.ViewModels.Shop 10 | { 11 | public class CustomerVM 12 | { 13 | public int Id { get; set; } 14 | public string? Name { get; set; } 15 | public string? Email { get; set; } 16 | public string? PhoneNumber { get; set; } 17 | public string? Address { get; set; } 18 | public string? City { get; set; } 19 | public string? Gender { get; set; } 20 | 21 | public ICollection? Orders { get; set; } 22 | } 23 | 24 | public class CustomerViewModelValidator : AbstractValidator 25 | { 26 | public CustomerViewModelValidator() 27 | { 28 | RuleFor(register => register.Name).NotEmpty().WithMessage("Customer name cannot be empty"); 29 | RuleFor(register => register.Gender).NotEmpty().WithMessage("Gender cannot be empty"); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /QuickApp.Server/ViewModels/Shop/OrderVM.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Server.ViewModels.Shop 8 | { 9 | public class OrderVM 10 | { 11 | public int Id { get; set; } 12 | public decimal Discount { get; set; } 13 | public string? Comments { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /QuickApp.Server/ViewModels/Shop/ProductVM.cs: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | namespace QuickApp.Server.ViewModels.Shop 8 | { 9 | public class ProductVM 10 | { 11 | public int Id { get; set; } 12 | public string? Name { get; set; } 13 | public string? Description { get; set; } 14 | public string? Icon { get; set; } 15 | public decimal BuyingPrice { get; set; } 16 | public decimal SellingPrice { get; set; } 17 | public int UnitsInStock { get; set; } 18 | public bool IsActive { get; set; } 19 | public bool IsDiscontinued { get; set; } 20 | public string? ProductCategoryName { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /QuickApp.Server/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=QuickApp;Trusted_Connection=True;TrustServerCertificate=true;MultipleActiveResultSets=true" 4 | }, 5 | 6 | "SmtpConfig": { 7 | "Host": "mail.example.com", 8 | "Port": 25, 9 | "UseSSL": false, 10 | "Name": "QuickApp Template", 11 | "Username": "your@email.com", 12 | "EmailAddress": "your@email.com", 13 | "Password": "YourPassword" 14 | }, 15 | 16 | // LogLevel Severity: "Trace", "Debug", "Information", "Warning", "Error", "Critical", "None" 17 | "Logging": { 18 | "PathFormat": "Logs/log-{Date}.log", 19 | "LogLevel": { 20 | "Default": "Information", 21 | "Microsoft.AspNetCore": "Warning" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /QuickApp.Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=(local);Database=QuickApp;Trusted_Connection=True;TrustServerCertificate=true;MultipleActiveResultSets=true" 4 | }, 5 | 6 | "SmtpConfig": { 7 | "Host": "mail.example.com", 8 | "Port": 25, 9 | "UseSSL": false, 10 | "Name": "QuickApp Template", 11 | "Username": "your@email.com", 12 | "EmailAddress": "your@email.com", 13 | "Password": "YourPassword" 14 | }, 15 | 16 | "OIDC": { 17 | "Certificates": { 18 | "Path": "", // e.g. "Certificates/quickapp.pfx" 19 | "Password": "" 20 | } 21 | }, 22 | 23 | // LogLevel Severity: "Trace", "Debug", "Information", "Warning", "Error", "Critical", "None" 24 | "Logging": { 25 | "PathFormat": "Logs/log-{Date}.log", 26 | "LogLevel": { 27 | "Default": "Information", 28 | "Microsoft.AspNetCore": "Warning" 29 | } 30 | }, 31 | "AllowedHosts": "*" 32 | } 33 | -------------------------------------------------------------------------------- /QuickApp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.12.35506.116 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickApp.Server", "QuickApp.Server\QuickApp.Server.csproj", "{E80B9546-1F09-404A-9721-7A9F8A999B7C}" 7 | EndProject 8 | Project("{54A90642-561A-4BB1-A94E-469ADEE60C69}") = "quickapp.client", "quickapp.client\quickapp.client.esproj", "{826A9202-C150-44C0-A1B6-91F8843BB14E}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickApp.Core", "QuickApp.Core\QuickApp.Core.csproj", "{3B0088D6-1A80-4806-84A3-D37D8581DB05}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FC2CFFC8-529D-408A-B7E5-A660AA8AF00C}" 13 | ProjectSection(SolutionItems) = preProject 14 | .gitignore = .gitignore 15 | LICENSE = LICENSE 16 | README.md = README.md 17 | EndProjectSection 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {E80B9546-1F09-404A-9721-7A9F8A999B7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {E80B9546-1F09-404A-9721-7A9F8A999B7C}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {E80B9546-1F09-404A-9721-7A9F8A999B7C}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {E80B9546-1F09-404A-9721-7A9F8A999B7C}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {826A9202-C150-44C0-A1B6-91F8843BB14E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {826A9202-C150-44C0-A1B6-91F8843BB14E}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {826A9202-C150-44C0-A1B6-91F8843BB14E}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 32 | {826A9202-C150-44C0-A1B6-91F8843BB14E}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {826A9202-C150-44C0-A1B6-91F8843BB14E}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {826A9202-C150-44C0-A1B6-91F8843BB14E}.Release|Any CPU.Deploy.0 = Release|Any CPU 35 | {3B0088D6-1A80-4806-84A3-D37D8581DB05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {3B0088D6-1A80-4806-84A3-D37D8581DB05}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {3B0088D6-1A80-4806-84A3-D37D8581DB05}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {3B0088D6-1A80-4806-84A3-D37D8581DB05}.Release|Any CPU.Build.0 = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(SolutionProperties) = preSolution 41 | HideSolutionNode = FALSE 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /quickapp.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 | ij_typescript_use_double_quotes = false 14 | 15 | [*.md] 16 | max_line_length = off 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /quickapp.client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | /.vs 23 | 24 | # Visual Studio Code 25 | .vscode/* 26 | !.vscode/settings.json 27 | !.vscode/tasks.json 28 | !.vscode/launch.json 29 | !.vscode/extensions.json 30 | .history/* 31 | 32 | # Miscellaneous 33 | /.angular/cache 34 | .sass-cache/ 35 | /connect.lock 36 | /coverage 37 | /libpeerconnection.log 38 | testem.log 39 | /typings 40 | 41 | # System files 42 | .DS_Store 43 | Thumbs.db 44 | -------------------------------------------------------------------------------- /quickapp.client/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /quickapp.client/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "chrome", 6 | "request": "launch", 7 | "name": "localhost (Chrome)", 8 | "url": "https://localhost:4200", 9 | "webRoot": "${workspaceFolder}" 10 | }, 11 | { 12 | "type": "edge", 13 | "request": "launch", 14 | "name": "localhost (Edge)", 15 | "url": "https://localhost:4200", 16 | "webRoot": "${workspaceFolder}" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /quickapp.client/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /quickapp.client/README.md: -------------------------------------------------------------------------------- 1 | # QuickappClient 2 | 3 | This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.0.2. 4 | 5 | ## Development server 6 | 7 | To start a local development server, run: 8 | 9 | ```bash 10 | ng serve 11 | ``` 12 | 13 | Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. 14 | 15 | ## Code scaffolding 16 | 17 | Angular CLI includes powerful code scaffolding tools. To generate a new component, run: 18 | 19 | ```bash 20 | ng generate component component-name 21 | ``` 22 | 23 | For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: 24 | 25 | ```bash 26 | ng generate --help 27 | ``` 28 | 29 | ## Building 30 | 31 | To build the project run: 32 | 33 | ```bash 34 | ng build 35 | ``` 36 | 37 | This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. 38 | 39 | ## Running unit tests 40 | 41 | To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command: 42 | 43 | ```bash 44 | ng test 45 | ``` 46 | 47 | ## Running end-to-end tests 48 | 49 | For end-to-end (e2e) testing, run: 50 | 51 | ```bash 52 | ng e2e 53 | ``` 54 | 55 | Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. 56 | 57 | ## Additional Resources 58 | 59 | For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. 60 | -------------------------------------------------------------------------------- /quickapp.client/aspnetcore-https.js: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | // This script sets up HTTPS for the application using the ASP.NET Core HTTPS certificate 8 | const fs = require('fs'); 9 | const spawn = require('child_process').spawn; 10 | const path = require('path'); 11 | 12 | const baseFolder = 13 | process.env.APPDATA !== undefined && process.env.APPDATA !== '' 14 | ? `${process.env.APPDATA}/ASP.NET/https` 15 | : `${process.env.HOME}/.aspnet/https`; 16 | 17 | const certificateArg = process.argv.map(arg => arg.match(/--name=(?.+)/i)).filter(Boolean)[0]; 18 | const certificateName = certificateArg ? certificateArg.groups.value : process.env.npm_package_name; 19 | 20 | if (!certificateName) { 21 | console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<> explicitly.') 22 | process.exit(-1); 23 | } 24 | 25 | const certFilePath = path.join(baseFolder, `${certificateName}.pem`); 26 | const keyFilePath = path.join(baseFolder, `${certificateName}.key`); 27 | 28 | if (!fs.existsSync(baseFolder)) { 29 | fs.mkdirSync(baseFolder, { recursive: true }); 30 | } 31 | 32 | if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { 33 | spawn('dotnet', [ 34 | 'dev-certs', 35 | 'https', 36 | '--export-path', 37 | certFilePath, 38 | '--format', 39 | 'Pem', 40 | '--no-password', 41 | ], { stdio: 'inherit', }) 42 | .on('exit', (code) => process.exit(code)); 43 | } -------------------------------------------------------------------------------- /quickapp.client/eslint.config.js: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | // @ts-check 8 | const eslint = require("@eslint/js"); 9 | const tseslint = require("typescript-eslint"); 10 | const angular = require("angular-eslint"); 11 | 12 | module.exports = tseslint.config( 13 | { 14 | files: ["**/*.ts"], 15 | extends: [ 16 | eslint.configs.recommended, 17 | ...tseslint.configs.recommended, 18 | ...tseslint.configs.stylistic, 19 | ...angular.configs.tsRecommended, 20 | ], 21 | processor: angular.processInlineTemplates, 22 | rules: { 23 | "@angular-eslint/directive-selector": [ 24 | "error", 25 | { 26 | type: "attribute", 27 | prefix: "app", 28 | style: "camelCase", 29 | }, 30 | ], 31 | "@angular-eslint/component-selector": [ 32 | "error", 33 | { 34 | type: "element", 35 | prefix: "app", 36 | style: "kebab-case", 37 | }, 38 | ], 39 | }, 40 | }, 41 | { 42 | files: ["**/*.html"], 43 | extends: [ 44 | ...angular.configs.templateRecommended, 45 | ...angular.configs.templateAccessibility, 46 | ], 47 | rules: {}, 48 | } 49 | ); 50 | -------------------------------------------------------------------------------- /quickapp.client/karma.conf.js: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | module.exports = function (config) { 8 | config.set({ 9 | basePath: '', 10 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 11 | plugins: [ 12 | require('karma-jasmine'), 13 | require('karma-chrome-launcher'), 14 | require('karma-jasmine-html-reporter'), 15 | require('karma-coverage'), 16 | require('@angular-devkit/build-angular/plugins/karma') 17 | ], 18 | client: { 19 | jasmine: { 20 | // you can add configuration options for Jasmine here 21 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 22 | // for example, you can disable the random execution with `random: false` 23 | // or set a specific seed with `seed: 4321` 24 | }, 25 | clearContext: false // leave Jasmine Spec Runner output visible in browser 26 | }, 27 | jasmineHtmlReporter: { 28 | suppressAll: true // removes the duplicated traces 29 | }, 30 | coverageReporter: { 31 | dir: require('path').join(__dirname, './coverage/'), 32 | subdir: '.', 33 | reporters: [ 34 | { type: 'html' }, 35 | { type: 'text-summary' } 36 | ] 37 | }, 38 | reporters: ['progress', 'kjhtml'], 39 | port: 9876, 40 | colors: true, 41 | logLevel: config.LOG_INFO, 42 | autoWatch: true, 43 | browsers: ['Chrome'], 44 | singleRun: false, 45 | restartOnFileChange: true, 46 | listenAddress: 'localhost', 47 | hostname: 'localhost' 48 | }); 49 | }; 50 | 51 | -------------------------------------------------------------------------------- /quickapp.client/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /quickapp.client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quickapp.client", 3 | "version": "9.19.0", 4 | "description": "ASP.NET Core 9.0/Angular 19 startup project template with complete login, user and role management. Plus other useful services for Quick Application Development", 5 | "author": { 6 | "name": "Ebenezer Monney", 7 | "email": "info@ebenmonney.com", 8 | "url": "https://www.ebenmonney.com/about" 9 | }, 10 | "license": "MIT", 11 | "homepage": "https://www.ebenmonney.com/quickapp", 12 | "repository": "https://github.com/emonney/quickapp.git", 13 | "scripts": { 14 | "ng": "ng", 15 | "start": "run-script-os", 16 | "build": "ng build", 17 | "watch": "ng build --watch --configuration development", 18 | "test": "ng test", 19 | "lint": "ng lint", 20 | "prestart": "node aspnetcore-https", 21 | "start:windows": "ng serve --ssl --ssl-cert \"%APPDATA%\\ASP.NET\\https\\%npm_package_name%.pem\" --ssl-key \"%APPDATA%\\ASP.NET\\https\\%npm_package_name%.key\" --host=127.0.0.1", 22 | "start:default": "ng serve --ssl --ssl-cert \"$HOME/.aspnet/https/${npm_package_name}.pem\" --ssl-key \"$HOME/.aspnet/https/${npm_package_name}.key\" --host=127.0.0.1" 23 | }, 24 | "private": true, 25 | "dependencies": { 26 | "@angular/animations": "^19.0.1", 27 | "@angular/cdk": "^19.0.1", 28 | "@angular/common": "^19.0.1", 29 | "@angular/compiler": "^19.0.1", 30 | "@angular/core": "^19.0.1", 31 | "@angular/forms": "^19.0.1", 32 | "@angular/platform-browser": "^19.0.1", 33 | "@angular/platform-browser-dynamic": "^19.0.1", 34 | "@angular/router": "^19.0.1", 35 | "@ng-bootstrap/ng-bootstrap": "^17.0.1", 36 | "@ng-select/ng-select": "^14.1.0", 37 | "@ngx-translate/core": "^16.0.3", 38 | "@popperjs/core": "^2.11.8", 39 | "@siemens/ngx-datatable": "^22.4.1", 40 | "bootstrap": "^5.3.3", 41 | "bootswatch": "^5.3.3", 42 | "chart.js": "^4.4.6", 43 | "font-awesome": "^4.7.0", 44 | "jest-editor-support": "*", 45 | "ng2-charts": "^7.0.0", 46 | "ngx-toasta": "^4.0.0", 47 | "run-script-os": "*", 48 | "rxjs": "~7.8.0", 49 | "tslib": "^2.3.0", 50 | "zone.js": "~0.15.0" 51 | }, 52 | "overrides": { 53 | "@ng-bootstrap/ng-bootstrap": { 54 | "@angular/common": "$@angular/common", 55 | "@angular/core": "$@angular/core", 56 | "@angular/forms": "$@angular/forms", 57 | "@angular/localize": "$@angular/localize" 58 | } 59 | }, 60 | "devDependencies": { 61 | "@angular-devkit/build-angular": "^19.0.2", 62 | "@angular/cli": "^19.0.2", 63 | "@angular/compiler-cli": "^19.0.1", 64 | "@angular/localize": "^19.0.1", 65 | "@types/jasmine": "~5.1.0", 66 | "@types/node": "^22.10.1", 67 | "angular-eslint": "19.0.0", 68 | "eslint": "^9.15.0", 69 | "jasmine-core": "~5.4.0", 70 | "karma": "~6.4.0", 71 | "karma-chrome-launcher": "~3.2.0", 72 | "karma-coverage": "~2.2.0", 73 | "karma-jasmine": "~5.1.0", 74 | "karma-jasmine-html-reporter": "~2.1.0", 75 | "typescript": "~5.6.2", 76 | "typescript-eslint": "8.16.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /quickapp.client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emonney/QuickApp/435f959f6d3ef510fef831e64cb6ebe03494a944/quickapp.client/public/favicon.ico -------------------------------------------------------------------------------- /quickapp.client/public/images/demo/banner1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emonney/QuickApp/435f959f6d3ef510fef831e64cb6ebe03494a944/quickapp.client/public/images/demo/banner1.png -------------------------------------------------------------------------------- /quickapp.client/public/images/demo/banner2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emonney/QuickApp/435f959f6d3ef510fef831e64cb6ebe03494a944/quickapp.client/public/images/demo/banner2.png -------------------------------------------------------------------------------- /quickapp.client/public/images/demo/banner3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emonney/QuickApp/435f959f6d3ef510fef831e64cb6ebe03494a944/quickapp.client/public/images/demo/banner3.png -------------------------------------------------------------------------------- /quickapp.client/public/images/demo/banner4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emonney/QuickApp/435f959f6d3ef510fef831e64cb6ebe03494a944/quickapp.client/public/images/demo/banner4.png -------------------------------------------------------------------------------- /quickapp.client/public/images/logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emonney/QuickApp/435f959f6d3ef510fef831e64cb6ebe03494a944/quickapp.client/public/images/logo-black.png -------------------------------------------------------------------------------- /quickapp.client/public/images/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emonney/QuickApp/435f959f6d3ef510fef831e64cb6ebe03494a944/quickapp.client/public/images/logo-white.png -------------------------------------------------------------------------------- /quickapp.client/public/styles-external.css: -------------------------------------------------------------------------------- 1 | #pre-bootstrap { 2 | background-color: #262626; 3 | bottom: 0; 4 | left: 0; 5 | position: fixed; 6 | right: 0; 7 | top: 0; 8 | z-index: 999999; 9 | } 10 | 11 | #pre-bootstrap div.messaging { 12 | color: #FFFFFF; 13 | font-family: monospace; 14 | left: 0; 15 | margin-top: -107px; 16 | position: absolute; 17 | right: 0; 18 | text-align: center; 19 | top: 50%; 20 | } 21 | 22 | #pre-bootstrap h1 { 23 | font-size: 26px; 24 | line-height: 35px; 25 | margin: 0 0 20px 0; 26 | } 27 | 28 | #pre-bootstrap p { 29 | font-size: 18px; 30 | line-height: 32px; 31 | margin: 0 0 0 0; 32 | } 33 | 34 | 35 | .prebootShow { 36 | opacity: 1 !important; 37 | } 38 | 39 | .prebootStep { 40 | opacity: 0; 41 | transition: .5s ease-in-out all; 42 | } 43 | 44 | 45 | /*loader*/ 46 | /* Absolute Center Spinner */ 47 | 48 | 49 | .cs-loader { 50 | height: 100%; 51 | width: 100%; 52 | } 53 | 54 | .cs-loader-inner { 55 | margin-top: -1rem; 56 | width: 100%; 57 | color: #FFF; 58 | padding: 0 100px; 59 | text-align: center; 60 | } 61 | 62 | .cs-loader-inner span { 63 | font-size: 20px; 64 | opacity: 0; 65 | display: inline-block; 66 | color: gray; 67 | } 68 | 69 | @keyframes lol { 70 | 0% { 71 | opacity: 0; 72 | transform: translateX(-300px); 73 | } 74 | 75 | 33% { 76 | opacity: 1; 77 | transform: translateX(0px); 78 | } 79 | 80 | 66% { 81 | opacity: 1; 82 | transform: translateX(0px); 83 | } 84 | 85 | 100% { 86 | opacity: 0; 87 | transform: translateX(300px); 88 | } 89 | } 90 | 91 | @-webkit-keyframes lol { 92 | 0% { 93 | opacity: 0; 94 | -webkit-transform: translateX(-300px); 95 | } 96 | 97 | 33% { 98 | opacity: 1; 99 | -webkit-transform: translateX(0px); 100 | } 101 | 102 | 66% { 103 | opacity: 1; 104 | -webkit-transform: translateX(0px); 105 | } 106 | 107 | 100% { 108 | opacity: 0; 109 | -webkit-transform: translateX(300px); 110 | } 111 | } 112 | 113 | .cs-loader-inner span:nth-child(6) { 114 | -webkit-animation: lol 3s infinite ease-in-out; 115 | animation: lol 3s infinite ease-in-out; 116 | } 117 | 118 | .cs-loader-inner span:nth-child(5) { 119 | -webkit-animation: lol 3s 100ms infinite ease-in-out; 120 | animation: lol 3s 100ms infinite ease-in-out; 121 | } 122 | 123 | .cs-loader-inner span:nth-child(4) { 124 | -webkit-animation: lol 3s 200ms infinite ease-in-out; 125 | animation: lol 3s 200ms infinite ease-in-out; 126 | } 127 | 128 | .cs-loader-inner span:nth-child(3) { 129 | -webkit-animation: lol 3s 300ms infinite ease-in-out; 130 | animation: lol 3s 300ms infinite ease-in-out; 131 | } 132 | 133 | .cs-loader-inner span:nth-child(2) { 134 | -webkit-animation: lol 3s 400ms infinite ease-in-out; 135 | animation: lol 3s 400ms infinite ease-in-out; 136 | } 137 | 138 | .cs-loader-inner span:nth-child(1) { 139 | -webkit-animation: lol 3s 500ms infinite ease-in-out; 140 | animation: lol 3s 500ms infinite ease-in-out; 141 | } 142 | -------------------------------------------------------------------------------- /quickapp.client/quickapp.client.esproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | npm start 4 | Jasmine 5 | 6 | false 7 | 8 | $(MSBuildProjectDirectory)\dist\quickapp.client\browser\ 9 | 10 | -------------------------------------------------------------------------------- /quickapp.client/src/app/app-error.handler.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Injectable, ErrorHandler } from '@angular/core'; 8 | 9 | @Injectable() 10 | export class AppErrorHandler extends ErrorHandler { 11 | constructor() { 12 | super(); 13 | } 14 | 15 | override handleError(error: Error) { 16 | if (confirm("Fatal Error!\nAn unresolved error has occurred. Do you want to reload the page to correct this?\n\n" + 17 | `Error: ${error.message}`)) { 18 | window.location.reload(); 19 | } 20 | 21 | super.handleError(error); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /quickapp.client/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | $footerHeight: 40px; 2 | 3 | .app-container { 4 | height: 100%; 5 | } 6 | 7 | main { 8 | height: calc(100% - #{$footerHeight}); 9 | padding-top: 60px; 10 | } 11 | 12 | .footer-height { 13 | height: $footerHeight; 14 | } 15 | 16 | footer { 17 | width: 100%; 18 | height: $footerHeight; 19 | line-height: $footerHeight; 20 | background-color: #f5f5f5; 21 | } 22 | 23 | .navbar-brand > img { 24 | height: 30px; 25 | } 26 | 27 | .navbar .nav-pills a:focus, 28 | .navbar .nav-pills a:hover { 29 | color: #575757; 30 | } 31 | 32 | .navbar .nav-pills > .active > a, 33 | .navbar .nav-pills > .active > a:hover, 34 | .navbar .nav-pills > .active > a:focus { 35 | background-color: #efefef; 36 | color: #808080; 37 | } 38 | 39 | .navbar .nav-link, 40 | .navbar .navbar-text { 41 | color: white; 42 | } 43 | 44 | 45 | .navbar .nav-pills > li > a { 46 | font-weight: bold; 47 | padding-top: 0.4rem; 48 | padding-bottom: 0.4rem; 49 | } 50 | 51 | 52 | @media screen and (max-width: 991px) { 53 | .navbar .nav-pills > li + li { 54 | margin-left: 0; 55 | margin-top: 1px; 56 | } 57 | } 58 | 59 | @media screen and (min-width: 992px) { 60 | .navbar .nav-pills > li + li { 61 | margin-left: 2px; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /quickapp.client/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { TestBed } from '@angular/core/testing'; 8 | 9 | import { appConfig } from './app.config'; 10 | import { AppComponent } from './app.component'; 11 | 12 | describe('AppComponent', () => { 13 | beforeEach(async () => { 14 | await TestBed.configureTestingModule({ 15 | imports: [AppComponent], 16 | ...appConfig 17 | }).compileComponents(); 18 | }); 19 | 20 | it('should create the app', () => { 21 | const fixture = TestBed.createComponent(AppComponent); 22 | const app = fixture.componentInstance; 23 | expect(app).toBeTruthy(); 24 | }); 25 | 26 | it(`should have the 'QuickApp' title`, () => { 27 | const fixture = TestBed.createComponent(AppComponent); 28 | const app = fixture.componentInstance; 29 | expect(app.appTitle).toEqual('QuickApp'); 30 | }); 31 | 32 | it('should render Loaded! in a h1 tag', () => { 33 | const fixture = TestBed.createComponent(AppComponent); 34 | fixture.detectChanges(); 35 | const compiled = fixture.nativeElement as HTMLElement; 36 | expect(compiled.querySelector('h1')?.textContent).toContain('Loaded!'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /quickapp.client/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { ApplicationConfig, ErrorHandler, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; 8 | import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; 9 | import { PreloadAllModules, provideRouter, TitleStrategy, UrlSerializer, withPreloading } from '@angular/router'; 10 | import { provideAnimations } from '@angular/platform-browser/animations'; 11 | 12 | import { provideCharts, withDefaultRegisterables } from 'ng2-charts'; 13 | import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; 14 | 15 | import { routes } from './app.routes'; 16 | import { AppErrorHandler } from './app-error.handler'; 17 | import { AppTitleService } from './services/app-title.service'; 18 | import { LowerCaseUrlSerializer } from './services/lowercase-url-serializer.service'; 19 | import { TranslateLanguageLoader } from './services/app-translation.service'; 20 | 21 | export const appConfig: ApplicationConfig = { 22 | providers: [ 23 | provideZoneChangeDetection({ eventCoalescing: true }), 24 | provideRouter(routes, withPreloading(PreloadAllModules)), 25 | provideHttpClient(withInterceptorsFromDi()), 26 | provideCharts(withDefaultRegisterables()), 27 | provideAnimations(), 28 | importProvidersFrom( 29 | TranslateModule.forRoot({ 30 | loader: { provide: TranslateLoader, useClass: TranslateLanguageLoader } 31 | }) 32 | ), 33 | { provide: ErrorHandler, useClass: AppErrorHandler }, 34 | { provide: TitleStrategy, useClass: AppTitleService }, 35 | { provide: UrlSerializer, useClass: LowerCaseUrlSerializer } 36 | ] 37 | }; 38 | -------------------------------------------------------------------------------- /quickapp.client/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Routes } from '@angular/router'; 8 | import { AuthGuard } from './services/auth-guard'; 9 | 10 | export const routes: Routes = [ 11 | { 12 | path: '', 13 | loadComponent: () => import('./components/home/home.component').then(m => m.HomeComponent), 14 | canActivate: [AuthGuard], 15 | title: 'Home' 16 | }, 17 | { 18 | path: 'login', 19 | loadComponent: () => import('./components/login/login.component').then(m => m.LoginComponent), 20 | title: 'Login' 21 | }, 22 | { 23 | path: 'customers', 24 | loadComponent: () => import('./components/customers/customers.component').then(m => m.CustomersComponent), 25 | canActivate: [AuthGuard], 26 | title: 'Customers' 27 | }, 28 | { 29 | path: 'products', 30 | loadComponent: () => import('./components/products/products.component').then(m => m.ProductsComponent), 31 | canActivate: [AuthGuard], 32 | title: 'Products' 33 | }, 34 | { 35 | path: 'orders', 36 | loadComponent: () => import('./components/orders/orders.component').then(m => m.OrdersComponent), 37 | canActivate: [AuthGuard], 38 | title: 'Orders' 39 | }, 40 | { 41 | path: 'settings', 42 | loadComponent: () => import('./components/settings/settings.component').then(m => m.SettingsComponent), 43 | canActivate: [AuthGuard], 44 | title: 'Settings' 45 | }, 46 | { 47 | path: 'about', 48 | loadComponent: () => import('./components/about/about.component').then(m => m.AboutComponent), 49 | title: 'About Us' 50 | }, 51 | { 52 | path: 'home', 53 | redirectTo: '/', 54 | pathMatch: 'full' 55 | }, 56 | { 57 | path: '**', 58 | loadComponent: () => import('./components/not-found/not-found.component').then(m => m.NotFoundComponent), 59 | title: 'Page Not Found' 60 | } 61 | ]; 62 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/styles/_custom-variables.scss: -------------------------------------------------------------------------------- 1 | //Override bootstrap and other variables here 2 | 3 | /*$container-max-widths: ( 4 | sm: 540px, 5 | md: 720px, 6 | lg: 960px, 7 | xl: 1250px, //1140px, 8 | xxl: 1320px 9 | )*/ 10 | 11 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/styles/alertify.default.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Default Look and Feel 3 | */ 4 | .alertify, 5 | .alertify-log { 6 | font-family: sans-serif; 7 | } 8 | .alertify { 9 | background: #FFF; 10 | border: 10px solid #333; /* browsers that don't support rgba */ 11 | border: 10px solid rgba(0,0,0,.7); 12 | border-radius: 8px; 13 | box-shadow: 0 3px 3px rgba(0,0,0,.3); 14 | -webkit-background-clip: padding; /* Safari 4? Chrome 6? */ 15 | -moz-background-clip: padding; /* Firefox 3.6 */ 16 | background-clip: padding-box; /* Firefox 4, Safari 5, Opera 10, IE 9 */ 17 | } 18 | .alertify-text { 19 | border: 1px solid #CCC; 20 | padding: 10px; 21 | border-radius: 4px; 22 | } 23 | .alertify-button { 24 | border-radius: 4px; 25 | color: #FFF; 26 | font-weight: bold; 27 | padding: 6px 15px; 28 | text-decoration: none; 29 | text-shadow: 1px 1px 0 rgba(0,0,0,.5); 30 | box-shadow: inset 0 1px 0 0 rgba(255,255,255,.5); 31 | background-image: -webkit-linear-gradient(to bottom, rgba(255,255,255,.3), rgba(255,255,255,0)); 32 | background-image: -moz-linear-gradient(to bottom, rgba(255,255,255,.3), rgba(255,255,255,0)); 33 | background-image: -ms-linear-gradient(to bottom, rgba(255,255,255,.3), rgba(255,255,255,0)); 34 | background-image: -o-linear-gradient(to bottom, rgba(255,255,255,.3), rgba(255,255,255,0)); 35 | background-image: linear-gradient(to bottom, rgba(255,255,255,.3), rgba(255,255,255,0)); 36 | } 37 | .alertify-button:hover, 38 | .alertify-button:focus { 39 | outline: none; 40 | background-image: -webkit-linear-gradient(to bottom, rgba(0,0,0,.1), rgba(0,0,0,0)); 41 | background-image: -moz-linear-gradient(to bottom, rgba(0,0,0,.1), rgba(0,0,0,0)); 42 | background-image: -ms-linear-gradient(to bottom, rgba(0,0,0,.1), rgba(0,0,0,0)); 43 | background-image: -o-linear-gradient(to bottom, rgba(0,0,0,.1), rgba(0,0,0,0)); 44 | background-image: linear-gradient(to bottom, rgba(0,0,0,.1), rgba(0,0,0,0)); 45 | } 46 | .alertify-button:focus { 47 | box-shadow: 0 0 15px #2B72D5; 48 | } 49 | .alertify-button:active { 50 | position: relative; 51 | box-shadow: inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05); 52 | } 53 | .alertify-button-cancel, 54 | .alertify-button-cancel:hover, 55 | .alertify-button-cancel:focus { 56 | background-color: #FE1A00; 57 | border: 1px solid #D83526; 58 | } 59 | .alertify-button-ok, 60 | .alertify-button-ok:hover, 61 | .alertify-button-ok:focus { 62 | background-color: #5CB811; 63 | border: 1px solid #3B7808; 64 | } 65 | 66 | .alertify-log { 67 | background: #1F1F1F; 68 | background: rgba(0,0,0,.9); 69 | padding: 15px; 70 | border-radius: 4px; 71 | color: #FFF; 72 | text-shadow: -1px -1px 0 rgba(0,0,0,.5); 73 | } 74 | .alertify-log-error { 75 | background: #FE1A00; 76 | background: rgba(254,26,0,.9); 77 | } 78 | .alertify-log-success { 79 | background: #5CB811; 80 | background: rgba(92,184,17,.9); 81 | } 82 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/styles/styles-vendor.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../node_modules/ngx-toasta/styles/style-bootstrap.css'; 2 | @use '../../../../node_modules/@siemens/ngx-datatable/index.css'; 3 | @use '../../../../node_modules/@siemens/ngx-datatable/assets/icons.css'; 4 | @use '../../../../node_modules/@ng-select/ng-select/themes/default.theme.css'; /* or material.theme.css*/ 5 | @use '../../../../node_modules/font-awesome/css/font-awesome.css'; 6 | @use 'alertify.core.css'; 7 | @use 'alertify.bootstrap.css' as alertify-bootstrap; 8 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/styles/vertical-tabs.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | 3 | $padding-base: 15px; 4 | $color-body: transparent; 5 | $color-border: #ddd; 6 | $color-right-border: white; 7 | $border-radius: .25rem; 8 | 9 | .nav-tabs { 10 | &--vertical { 11 | border-bottom: none !important; 12 | border-right: 1px solid $color-border; 13 | display: flex; 14 | flex-flow: column nowrap; 15 | } 16 | 17 | &--left { 18 | margin: 0 $padding-base; 19 | 20 | .nav-item + .nav-item { 21 | margin-top: .25rem; 22 | } 23 | 24 | .nav-link { 25 | border-radius: $border-radius 0 0 $border-radius !important; 26 | transition: border-color .125s ease-in; 27 | white-space: nowrap; 28 | 29 | &:hover { 30 | background-color: color.adjust($color-border, $lightness: 10%); 31 | border-color: transparent; 32 | } 33 | } 34 | 35 | .nav-link.active { 36 | border-color: $color-border !important; 37 | border-right-color: $color-right-border !important; 38 | margin-right: -1px; 39 | 40 | &:hover { 41 | cursor: default; 42 | background-color: $color-body; 43 | border-color: $color-border $color-right-border $color-border $color-border; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/_app-theme.scss: -------------------------------------------------------------------------------- 1 | 2 | .ngx-datatable.material { 3 | &.colored-header { 4 | .datatable-header { 5 | padding: .5rem 0; 6 | color: map-get($theme-colors, "light"); 7 | background-color: map-get($theme-colors, "primary"); 8 | } 9 | } 10 | } 11 | 12 | .nav-tabs { 13 | &--left { 14 | .nav-link.active { 15 | border-right-color: $body-bg !important; 16 | 17 | &:hover { 18 | border-right-color: $body-bg !important; 19 | } 20 | } 21 | } 22 | } 23 | 24 | 25 | .about-page { 26 | .bg-features { 27 | @if $enable-rounded { 28 | border-radius: $border-radius !important; 29 | } 30 | @else { 31 | border-radius: 0 !important; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/bootswatch_fix/_cosmo.scss: -------------------------------------------------------------------------------- 1 | // Cosmo 5.3.3 2 | // Bootswatch 3 | 4 | 5 | // Variables 6 | 7 | /* 8 | This is a temporary fix for the issue with importing the font path in the theme. 9 | Once it is fixed delete this file and import from node_modules "bootswatch/dist//bootswatch" file instead. 10 | */ 11 | 12 | $web-font-path: "https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;700&display=swap" !default; 13 | 14 | @if $web-font-path { 15 | @import url("https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;700&display=swap"); 16 | } 17 | 18 | // Typography 19 | 20 | body { 21 | -webkit-font-smoothing: antialiased; 22 | } 23 | 24 | // Indicators 25 | 26 | .badge { 27 | &.bg-light { 28 | color: $dark; 29 | } 30 | } 31 | 32 | // Progress bars 33 | 34 | .progress { 35 | @include box-shadow(none); 36 | 37 | .progress-bar { 38 | font-size: 8px; 39 | line-height: 8px; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/bootswatch_fix/_flatly.scss: -------------------------------------------------------------------------------- 1 | // Flatly 5.3.3 2 | // Bootswatch 3 | 4 | 5 | // Variables 6 | 7 | /* 8 | This is a temporary fix for the issue with importing the font path in the theme. 9 | Once it is fixed delete this file and import from node_modules "bootswatch/dist//bootswatch" file instead. 10 | */ 11 | 12 | $web-font-path: "https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400&display=swap" !default; 13 | 14 | @if $web-font-path { 15 | @import url("https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400&display=swap"); 16 | } 17 | 18 | // Navs 19 | 20 | .pagination { 21 | a:hover { 22 | text-decoration: none; 23 | } 24 | } 25 | 26 | // Indicators 27 | 28 | .badge { 29 | &.bg-light { 30 | color: $dark; 31 | } 32 | } 33 | 34 | .alert { 35 | color: $white; 36 | border: none; 37 | 38 | a, 39 | .alert-link { 40 | color: $white; 41 | text-decoration: underline; 42 | } 43 | 44 | @each $color, $value in $theme-colors { 45 | &-#{$color} { 46 | @if $enable-gradients { 47 | background: $value linear-gradient(180deg, mix($body-bg, $value, 15%), $value) repeat-x; 48 | } 49 | @else { 50 | background-color: $value; 51 | } 52 | } 53 | } 54 | 55 | &-light { 56 | &, 57 | a, 58 | .alert-link { 59 | color: $body-color; 60 | } 61 | } 62 | } 63 | 64 | // Containers 65 | 66 | .modal, 67 | .toast, 68 | .offcanvas { 69 | .btn-close { 70 | background-image: escape-svg(url("data:image/svg+xml,")); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/bootswatch_fix/_journal.scss: -------------------------------------------------------------------------------- 1 | // Journal 5.3.3 2 | // Bootswatch 3 | 4 | 5 | // Variables 6 | 7 | /* 8 | This is a temporary fix for the issue with importing the font path in the theme. 9 | Once it is fixed delete this file and import from node_modules "bootswatch/dist//bootswatch" file instead. 10 | */ 11 | 12 | $web-font-path: "https://fonts.googleapis.com/css2?family=News+Cycle:wght@400;700&display=swap" !default; 13 | 14 | @if $web-font-path { 15 | @import url("https://fonts.googleapis.com/css2?family=News+Cycle:wght@400;700&display=swap"); 16 | } 17 | 18 | // Navbar 19 | 20 | .navbar { 21 | font-family: $headings-font-family; 22 | font-size: 18px; 23 | font-weight: $headings-font-weight; 24 | } 25 | 26 | .navbar-brand { 27 | padding-top: .5rem; 28 | font-size: inherit; 29 | font-weight: $headings-font-weight; 30 | text-transform: uppercase; 31 | } 32 | 33 | // Buttons 34 | 35 | .btn { 36 | font-family: $headings-font-family; 37 | font-weight: $headings-font-weight; 38 | 39 | &-secondary, 40 | &-warning { 41 | color: $white; 42 | } 43 | } 44 | 45 | // Navs 46 | 47 | .pagination { 48 | a:hover { 49 | text-decoration: none; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/bootswatch_fix/_minty.scss: -------------------------------------------------------------------------------- 1 | // Minty 5.3.3 2 | // Bootswatch 3 | 4 | 5 | // Variables 6 | 7 | /* 8 | This is a temporary fix for the issue with importing the font path in the theme. 9 | Once it is fixed delete this file and import from node_modules "bootswatch/dist//bootswatch" file instead. 10 | */ 11 | 12 | $web-font-path: "https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;700&display=swap" !default; 13 | 14 | @if $web-font-path { 15 | @import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;700&display=swap"); 16 | } 17 | 18 | // Navbar 19 | 20 | .navbar { 21 | font-family: $headings-font-family; 22 | } 23 | 24 | // Buttons 25 | 26 | .btn { 27 | font-family: $headings-font-family; 28 | 29 | &, 30 | &:hover { 31 | color: $white; 32 | } 33 | 34 | &-light, 35 | &-light:hover { 36 | color: $gray-700; 37 | } 38 | 39 | &-link, 40 | &-link:hover { 41 | color: $primary; 42 | } 43 | 44 | &-link.disabled:hover { 45 | color: $gray-600; 46 | } 47 | 48 | &-outline-primary { 49 | color: $primary; 50 | } 51 | 52 | &-outline-secondary { 53 | color: $secondary; 54 | } 55 | 56 | &-outline-success { 57 | color: $success; 58 | } 59 | 60 | &-outline-info { 61 | color: $info; 62 | } 63 | 64 | &-outline-warning { 65 | color: $warning; 66 | } 67 | 68 | &-outline-danger { 69 | color: $danger; 70 | } 71 | 72 | &-outline-dark { 73 | color: $dark; 74 | } 75 | 76 | &-outline-light { 77 | color: $light; 78 | } 79 | } 80 | 81 | // Forms 82 | 83 | legend { 84 | font-family: $headings-font-family; 85 | } 86 | 87 | // Navs 88 | 89 | .dropdown-menu { 90 | font-family: $font-family-sans-serif; 91 | } 92 | 93 | .breadcrumb { 94 | a { 95 | color: $navbar-dark-color; 96 | } 97 | 98 | a:hover { 99 | color: $white; 100 | text-decoration: none; 101 | } 102 | } 103 | 104 | // Indicators 105 | 106 | .alert { 107 | a, 108 | .alert-link { 109 | color: $white; 110 | } 111 | 112 | &-light { 113 | 114 | &, 115 | a:not(.btn), 116 | .alert-link { 117 | color: $body-color; 118 | } 119 | } 120 | } 121 | 122 | .badge { 123 | color: $white; 124 | 125 | &.bg-light { 126 | color: $gray-700; 127 | } 128 | } 129 | 130 | // Containers 131 | 132 | .card, 133 | .list-group-item { 134 | h1, 135 | h2, 136 | h3, 137 | h4, 138 | h5, 139 | h6 { 140 | color: inherit; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/bootswatch_fix/_solar.scss: -------------------------------------------------------------------------------- 1 | // Solar 5.3.3 2 | // Bootswatch 3 | 4 | 5 | // Variables 6 | 7 | /* 8 | This is a temporary fix for the issue with importing the font path in the theme. 9 | Once it is fixed delete this file and import from node_modules "bootswatch/dist//bootswatch" file instead. 10 | */ 11 | 12 | $web-font-path: "https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap" !default; 13 | 14 | @if $web-font-path { 15 | @import url("https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"); 16 | } 17 | 18 | // Buttons 19 | 20 | .btn { 21 | @each $color, $value in $theme-colors { 22 | &-#{$color} { 23 | @if $enable-gradients { 24 | background: $value linear-gradient(180deg, mix($white, $value, 15%), $value) repeat-x; 25 | } 26 | @else { 27 | background-color: $value; 28 | } 29 | } 30 | } 31 | } 32 | 33 | // Indicators 34 | 35 | .alert { 36 | color: $white; 37 | border: none; 38 | 39 | a, 40 | .alert-link { 41 | color: $white; 42 | text-decoration: underline; 43 | } 44 | 45 | @each $color, $value in $theme-colors { 46 | &-#{$color} { 47 | @if $enable-gradients { 48 | background: $value linear-gradient(180deg, mix($white, $value, 15%), $value) repeat-x; 49 | } 50 | @else { 51 | background-color: $value; 52 | } 53 | } 54 | } 55 | 56 | &-light { 57 | &, 58 | a:not(.btn), 59 | .alert-link { 60 | color: $body-bg; 61 | } 62 | } 63 | } 64 | 65 | .badge { 66 | &.bg-light { 67 | color: $dark; 68 | } 69 | } 70 | 71 | .tooltip { 72 | --bs-tooltip-bg: var(--bs-tertiary-bg); 73 | --bs-tooltip-color: var(--bs-emphasis-color); 74 | } 75 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/bootswatch_fix/_spacelab.scss: -------------------------------------------------------------------------------- 1 | // Spacelab 5.3.3 2 | // Bootswatch 3 | 4 | 5 | // Variables 6 | 7 | /* 8 | This is a temporary fix for the issue with importing the font path in the theme. 9 | Once it is fixed delete this file and import from node_modules "bootswatch/dist//bootswatch" file instead. 10 | */ 11 | 12 | $web-font-path: "https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" !default; 13 | 14 | @if $web-font-path { 15 | @import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"); 16 | } 17 | 18 | // Mixins 19 | 20 | @mixin btn-shadow($color) { 21 | @include gradient-y-three-colors(tint-color($color, 24%), $color, 50%, shade-color($color, 8%)); 22 | filter: none; 23 | border: 1px solid shade-color($color, 20%); 24 | } 25 | 26 | // Navbar 27 | 28 | .navbar { 29 | .nav-link, 30 | .navbar-brand { 31 | text-shadow: -1px -1px 0 rgba(0, 0, 0, .05); 32 | transition: color ease-in-out .2s; 33 | } 34 | 35 | @each $color, $value in $theme-colors { 36 | &.bg-#{$color} { 37 | @include btn-shadow($value); 38 | } 39 | } 40 | 41 | &.bg-light { 42 | .nav-link, 43 | .navbar-brand { 44 | text-shadow: 1px 1px 0 rgba(255, 255, 255, .1); 45 | } 46 | 47 | .navbar-brand { 48 | color: $navbar-light-color; 49 | 50 | &:hover { 51 | color: $info; 52 | } 53 | } 54 | } 55 | } 56 | 57 | // Buttons 58 | 59 | .btn { 60 | text-shadow: -1px -1px 0 rgba(0, 0, 0, .1); 61 | 62 | &-link { 63 | text-shadow: none; 64 | } 65 | } 66 | 67 | @each $color, $value in $theme-colors { 68 | .btn-#{$color} { 69 | @include btn-shadow($value); 70 | } 71 | 72 | .btn-#{$color}:not(.disabled):hover { 73 | @include btn-shadow(shade-color($value, 8%)); 74 | } 75 | } 76 | 77 | [class*="btn-outline-"] { 78 | text-shadow: none; 79 | } 80 | 81 | // Indicators 82 | 83 | .badge { 84 | &.bg-light { 85 | color: $dark; 86 | } 87 | } 88 | 89 | // Containers 90 | 91 | .card, 92 | .list-group-item { 93 | h1, 94 | h2, 95 | h3, 96 | h4, 97 | h5, 98 | h6 { 99 | color: inherit; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/bootswatch_fix/_superhero.scss: -------------------------------------------------------------------------------- 1 | // Superhero 5.3.3 2 | // Bootswatch 3 | 4 | 5 | // Variables 6 | 7 | /* 8 | This is a temporary fix for the issue with importing the font path in the theme. 9 | Once it is fixed delete this file and import from node_modules "bootswatch/dist//bootswatch" file instead. 10 | */ 11 | 12 | $web-font-path: "https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap" !default; 13 | 14 | @if $web-font-path { 15 | @import url("https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap"); 16 | } 17 | 18 | // Buttons 19 | 20 | .btn { 21 | @each $color, $value in $theme-colors { 22 | &-#{$color} { 23 | @if $enable-gradients { 24 | background: $value linear-gradient(180deg, mix($white, $value, 15%), $value) repeat-x; 25 | } 26 | @else { 27 | background-color: $value; 28 | } 29 | } 30 | } 31 | } 32 | 33 | // Typography 34 | 35 | .dropdown-menu { 36 | font-size: $font-size-sm; 37 | } 38 | 39 | .dropdown-header { 40 | font-size: $font-size-sm; 41 | } 42 | 43 | .blockquote-footer { 44 | color: $body-color; 45 | } 46 | 47 | // Tables 48 | 49 | .table { 50 | font-size: $font-size-sm; 51 | 52 | .thead-dark th { 53 | color: $white; 54 | } 55 | 56 | a:not(.btn) { 57 | color: $white; 58 | text-decoration: underline; 59 | } 60 | 61 | .dropdown-menu a { 62 | text-decoration: none; 63 | } 64 | 65 | .text-muted { 66 | color: $text-muted; 67 | } 68 | } 69 | 70 | // Forms 71 | 72 | label, 73 | .radio label, 74 | .checkbox label, 75 | .help-block { 76 | font-size: $font-size-sm; 77 | } 78 | 79 | .form-floating { 80 | > label, 81 | > .form-control:focus ~ label, 82 | > .form-control:not(:placeholder-shown) ~ label { 83 | color: $input-placeholder-color; 84 | } 85 | } 86 | 87 | // Navs 88 | 89 | .nav-tabs, 90 | .nav-pills { 91 | .nav-link, 92 | .nav-link:hover { 93 | color: $body-color; 94 | } 95 | 96 | .nav-link.disabled { 97 | color: $nav-link-disabled-color; 98 | } 99 | } 100 | 101 | .page-link:hover, 102 | .page-link:focus { 103 | color: $white; 104 | text-decoration: none; 105 | } 106 | 107 | // Indicators 108 | 109 | .alert { 110 | color: $white; 111 | border: none; 112 | 113 | a, 114 | .alert-link { 115 | color: $white; 116 | text-decoration: underline; 117 | } 118 | 119 | @each $color, $value in $theme-colors { 120 | &-#{$color} { 121 | @if $enable-gradients { 122 | background: $value linear-gradient(180deg, mix($white, $value, 15%), $value) repeat-x; 123 | } 124 | @else { 125 | background-color: $value; 126 | } 127 | } 128 | } 129 | } 130 | 131 | .badge { 132 | &-warning, 133 | &-info { 134 | color: $white; 135 | } 136 | } 137 | 138 | .tooltip { 139 | --bs-tooltip-bg: var(--bs-tertiary-bg); 140 | --bs-tooltip-color: var(--bs-emphasis-color); 141 | } 142 | 143 | // Popovers 144 | 145 | .popover-header { 146 | border-top-left-radius: 0; 147 | border-top-right-radius: 0; 148 | } 149 | 150 | // Containers 151 | 152 | .modal { 153 | &-header, 154 | &-footer { 155 | background-color: $table-hover-bg; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/bootswatch_fix/_united.scss: -------------------------------------------------------------------------------- 1 | // United 5.3.3 2 | // Bootswatch 3 | 4 | 5 | // Variables 6 | 7 | /* 8 | This is a temporary fix for the issue with importing the font path in the theme. 9 | Once it is fixed delete this file and import from node_modules "bootswatch/dist//bootswatch" file instead. 10 | */ 11 | 12 | $web-font-path: "https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;700&display=swap" !default; 13 | 14 | @if $web-font-path { 15 | @import url("https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;700&display=swap"); 16 | } 17 | 18 | // Indicators 19 | 20 | .badge { 21 | &.bg-light { 22 | color: $body-color; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/cerulean.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/custom-variables'; 2 | 3 | @import 'bootswatch/dist/cerulean/variables'; 4 | @import 'bootstrap/scss/bootstrap'; 5 | @import 'bootswatch/dist/cerulean/bootswatch'; 6 | 7 | @import 'app-theme'; 8 | 9 | .app-component { 10 | .navbar { 11 | .popover-header { 12 | color: $headings-color !important; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/cosmo.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/custom-variables'; 2 | 3 | @import 'bootswatch/dist/cosmo/variables'; 4 | @import 'bootstrap/scss/bootstrap'; 5 | @import 'bootswatch_fix/cosmo'; // import from 'bootswatch/dist/cosmo/bootswatch' when font import issue is fixed 6 | 7 | @import 'app-theme'; 8 | 9 | 10 | .icon-addon #searchInput.form-control { 11 | border-radius: 5px; 12 | } 13 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/flatly.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/custom-variables'; 2 | 3 | $table-bg-scale: 0% !default; // Fix for deprecation: https://sass-lang.com/d/function-units 4 | 5 | @import 'bootswatch/dist/flatly/variables'; 6 | @import 'bootstrap/scss/bootstrap'; 7 | @import 'bootswatch_fix/flatly'; // import from 'bootswatch/dist/flatly/bootswatch' when font import issue is fixed 8 | 9 | @import 'app-theme'; 10 | 11 | 12 | .app-component { 13 | .nav-link { 14 | padding-left: 1rem; 15 | padding-right: 1rem; 16 | } 17 | 18 | main { 19 | padding-top: 80px !important; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/journal.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/custom-variables'; 2 | 3 | @import 'bootswatch/dist/journal/variables'; 4 | @import 'bootstrap/scss/bootstrap'; 5 | @import 'bootswatch_fix/journal'; // import from 'bootswatch/dist/journal/bootswatch' when font import issue is fixed 6 | 7 | @import 'app-theme'; 8 | 9 | 10 | .about-page { 11 | .bg-features { 12 | .logo { 13 | color: $primary !important; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/lumen.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/custom-variables'; 2 | 3 | $table-bg-scale: 0% !default; // Fix for deprecation: https://sass-lang.com/d/function-units 4 | 5 | @import 'bootswatch/dist/lumen/variables'; 6 | @import 'bootstrap/scss/bootstrap'; 7 | @import 'bootswatch_fix/lumen'; // import from 'bootswatch/dist/lumen/bootswatch' when font import issue is fixed 8 | 9 | @import 'app-theme'; 10 | 11 | 12 | .about-page { 13 | .bg-features { 14 | .logo { 15 | color: #fbfbfb !important; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/minty.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/custom-variables'; 2 | 3 | $table-bg-scale: 0% !default; // Fix for deprecation: https://sass-lang.com/d/function-units 4 | $alert-bg-scale: 0% !default; // Fix for deprecation: https://sass-lang.com/d/function-units 5 | 6 | @import 'bootswatch/dist/minty/variables'; 7 | @import 'bootstrap/scss/bootstrap'; 8 | @import 'bootswatch_fix/minty'; // import from 'bootswatch/dist/minty/bootswatch' when font import issue is fixed 9 | 10 | @import 'app-theme'; 11 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/pulse.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/custom-variables'; 2 | 3 | @import 'bootswatch/dist/pulse/variables'; 4 | @import 'bootstrap/scss/bootstrap'; 5 | @import 'bootswatch/dist/pulse/bootswatch'; 6 | 7 | @import 'app-theme'; 8 | 9 | 10 | .icon-addon #searchInput.form-control { 11 | border-radius: 0; 12 | } 13 | 14 | 15 | .app-component { 16 | main { 17 | padding-top: 90px !important; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/sketchy.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/custom-variables'; 2 | 3 | @import 'bootswatch/dist/sketchy/variables'; 4 | @import 'bootstrap/scss/bootstrap'; 5 | @import 'bootswatch_fix/sketchy'; // import from 'bootswatch/dist/sketchy/bootswatch' when font import issue is fixed 6 | 7 | @import 'app-theme'; 8 | 9 | 10 | input[type="radio"], input[type="checkbox"] { 11 | width: 0; 12 | height: 0; 13 | margin-right: -40px; 14 | margin-left: 0; 15 | } 16 | 17 | .card { 18 | overflow: visible; 19 | } 20 | 21 | .login-container .card-header { 22 | .btn-close::before { 23 | right: 0; 24 | top: 0; 25 | font-size: 1.2rem; 26 | color: initial; 27 | } 28 | } 29 | 30 | .bg-light { 31 | background-color: #f8f9fa !important; 32 | } 33 | 34 | 35 | .about-page { 36 | .bg-features { 37 | .logo { 38 | color: #f1f1f1 !important; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/slate.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/custom-variables'; 2 | 3 | $table-bg-scale: 0% !default; // Fix for deprecation: https://sass-lang.com/d/function-units 4 | 5 | @import 'bootswatch/dist/slate/variables'; 6 | @import 'bootstrap/scss/bootstrap'; 7 | @import 'bootswatch/dist/slate/bootswatch'; 8 | 9 | @import 'app-theme'; 10 | 11 | 12 | .bg-light { 13 | background-color: $primary !important; 14 | } 15 | 16 | .tooltip { 17 | --bs-tooltip-color: #fff; 18 | } 19 | 20 | .ngx-datatable.material.table-striped { 21 | .datatable-row-even { 22 | background: $gray-700; 23 | } 24 | } 25 | 26 | .alertify-message { 27 | color: var(--bs-secondary); 28 | } 29 | 30 | .login-container, 31 | .register-container, 32 | .recover-container, 33 | .reset-container { 34 | color: var(--bs-secondary); 35 | 36 | .card a { 37 | color: #5bc0de; 38 | } 39 | 40 | .btn-social.btn-outline-primary { 41 | color: initial; 42 | } 43 | } 44 | 45 | .app-component { 46 | .navbar a.user-name { 47 | border: none; 48 | } 49 | 50 | footer { 51 | background-color: $primary !important; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/solar.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/custom-variables'; 2 | 3 | $table-bg-scale: 0% !default; // Fix for deprecation: https://sass-lang.com/d/function-units 4 | 5 | @import 'bootswatch/dist/solar/variables'; 6 | @import 'bootstrap/scss/bootstrap'; 7 | @import 'bootswatch_fix/solar'; // import from 'bootswatch/dist/solar/bootswatch' when font import issue is fixed 8 | 9 | @import 'app-theme'; 10 | 11 | .ngx-datatable.material { 12 | &.table-striped { 13 | .datatable-row-even { 14 | background: $dark; 15 | } 16 | } 17 | } 18 | 19 | .tooltip { 20 | --bs-tooltip-color: #fff; 21 | } 22 | 23 | 24 | .bg-light { 25 | background-color: #04272f !important; 26 | } 27 | 28 | 29 | .app-component { 30 | .login-container .form-check-input { 31 | border-color: var(--bs-body-color); 32 | } 33 | 34 | footer { 35 | background-color: #073642 !important; 36 | } 37 | } 38 | 39 | .about-page { 40 | .bg-features { 41 | .logo { 42 | color: #073642 !important; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/spacelab.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/custom-variables'; 2 | 3 | @import 'bootswatch/dist/spacelab/variables'; 4 | @import 'bootstrap/scss/bootstrap'; 5 | @import 'bootswatch_fix/spacelab'; // import from 'bootswatch/dist/spacelab/bootswatch' when font import issue is fixed 6 | 7 | @import 'app-theme'; 8 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/superhero.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/custom-variables'; 2 | 3 | $table-bg-scale: 0% !default; // Fix for deprecation: https://sass-lang.com/d/function-units 4 | 5 | @import 'bootswatch/dist/superhero/variables'; 6 | @import 'bootstrap/scss/bootstrap'; 7 | @import 'bootswatch_fix/superhero'; // import from 'bootswatch/dist/superhero/bootswatch' when font import issue is fixed 8 | 9 | @import 'app-theme'; 10 | 11 | 12 | .ngx-datatable.material { 13 | &.table-striped { 14 | .datatable-row-even { 15 | background: $secondary; 16 | } 17 | } 18 | 19 | &.table-hover:not(.cell-selection) { 20 | .datatable-body-row:hover, 21 | .datatable-body-row:hover .datatable-row-group { 22 | background-color: $gray-600 !important; 23 | } 24 | } 25 | 26 | &.colored-header { 27 | .datatable-header { 28 | color: $body-bg; 29 | } 30 | } 31 | } 32 | 33 | .tooltip { 34 | --bs-tooltip-color: #fff; 35 | } 36 | 37 | .alertify-message { 38 | color: var(--bs-secondary); 39 | } 40 | 41 | .bg-light { 42 | background-color: var(--bs-dark) !important; 43 | } 44 | 45 | .nav-tabs--vertical .nav-link:hover:not(.active) { 46 | color: #3d7cba; 47 | } 48 | 49 | .app-component { 50 | .login-container, 51 | .register-container, 52 | .recover-container, 53 | .reset-container { 54 | color: var(--bs-dark); 55 | 56 | .form-control { 57 | background-color: var(--bs-gray-100); 58 | } 59 | 60 | .form-control-plaintext { 61 | color: var(--bs-secondary); 62 | } 63 | 64 | .form-check-input { 65 | border: 1px solid var(--bs-primary); 66 | } 67 | } 68 | 69 | footer { 70 | background-color: $gray-700 !important; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /quickapp.client/src/app/assets/themes/united.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/custom-variables'; 2 | 3 | @import 'bootswatch/dist/united/variables'; 4 | @import 'bootstrap/scss/bootstrap'; 5 | @import 'bootswatch_fix/united'; // import from 'bootswatch/dist/united/bootswatch' when font import issue is fixed 6 | 7 | @import 'app-theme'; 8 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/about/about.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 57 |
58 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/about/about.component.scss: -------------------------------------------------------------------------------- 1 | .reduced-font { 2 | line-height: 1.4; 3 | } 4 | 5 | .bg-features { 6 | position: relative; 7 | border-radius: 0.25rem; 8 | padding: 0.5rem; 9 | margin-bottom: 0.5rem; 10 | } 11 | 12 | .bg-features ul { 13 | position: relative; 14 | } 15 | 16 | .bg-features .logo { 17 | font-size: 200px; 18 | position: absolute; 19 | top: 2rem; 20 | right: 10px; 21 | } 22 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/about/about.component.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Component } from '@angular/core'; 8 | import { TranslateModule } from '@ngx-translate/core'; 9 | 10 | import { fadeInOut } from '../../services/animations'; 11 | 12 | @Component({ 13 | selector: 'app-about', 14 | templateUrl: './about.component.html', 15 | styleUrl: './about.component.scss', 16 | animations: [fadeInOut], 17 | imports: [TranslateModule] 18 | }) 19 | export class AboutComponent { 20 | } 21 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/banner-demo.component.scss: -------------------------------------------------------------------------------- 1 | @import 'bootstrap/scss/functions'; 2 | @import 'bootstrap/scss/variables'; 3 | @import 'bootstrap/scss/mixins'; 4 | 5 | .carousel-slide-content { 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | } 10 | 11 | .carousel-caption { 12 | margin-top: 2vw; 13 | text-align: center; 14 | font-size: 1.1vw; 15 | bottom: auto; 16 | 17 | .btn.btn-sm { 18 | padding: 0 5px 2px 5px; 19 | font-weight: bold; 20 | } 21 | 22 | @include media-breakpoint-down(md) { 23 | display: none; 24 | } 25 | 26 | @include media-breakpoint-up(xxl) { 27 | margin-top: 20px; 28 | font-size: 1rem; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/banner-demo.component.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Component } from '@angular/core'; 8 | import { NgClass } from '@angular/common'; 9 | import { NgbCarousel, NgbSlide } from '@ng-bootstrap/ng-bootstrap'; 10 | 11 | @Component({ 12 | selector: 'app-banner-demo', 13 | templateUrl: './banner-demo.component.html', 14 | styleUrl: './banner-demo.component.scss', 15 | imports: [NgbCarousel, NgbSlide, NgClass] 16 | }) 17 | export class BannerDemoComponent { 18 | 19 | carouselSlides = [ 20 | { 21 | img: 'images/demo/banner1.png', 22 | alt: 'ASP.NET', 23 | caption: 'Learn how to build ASP.NET apps that can run anywhere', 24 | class: 'btn btn-outline-info btn-sm', 25 | link: 'http://go.microsoft.com/fwlink/?LinkID=525028&clcid=0x409' 26 | }, 27 | { 28 | img: 'images/demo/banner2.png', 29 | alt: 'Visual Studio', 30 | caption: 'One platform for building modern web, native mobile and native desktop applications', 31 | class: 'btn btn-outline-primary btn-sm', 32 | link: 'http://angular.dev' 33 | }, 34 | { 35 | img: 'images/demo/banner3.png', 36 | alt: 'Package Management', 37 | caption: 'Bring in libraries from NuGet and npm, and bundle with angular/cli', 38 | class: 'btn btn-outline-success btn-sm', 39 | link: 'http://go.microsoft.com/fwlink/?LinkID=525029&clcid=0x409' 40 | }, 41 | { 42 | img: 'images/demo/banner4.png', 43 | alt: 'Eben Monney', 44 | caption: 'Follow me on social media for updates and tips on using this startup project', 45 | class: 'btn btn-outline-secondary btn-sm', 46 | link: 'https://www.ebenmonney.com/about' 47 | } 48 | ]; 49 | } 50 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/notifications-viewer.component.html: -------------------------------------------------------------------------------- 1 |
2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {{getPrintedDate(value)}} 25 | 26 | 27 | 28 | 29 | 30 | {{value}} 31 | 32 | 33 | 34 | 35 | 36 | {{value}} 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/notifications-viewer.component.scss: -------------------------------------------------------------------------------- 1 |  2 | .unread { 3 | font-weight: bold; 4 | } 5 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/role-editor.component.scss: -------------------------------------------------------------------------------- 1 | .row:not(:last-child) { 2 | /*border-bottom: 1px solid #ccc;*/ 3 | } 4 | 5 | .edit-separator { 6 | margin: 10px 5px; 7 | border-top: 1px dashed currentColor; 8 | background-color: inherit; 9 | } 10 | 11 | .last-edit-separator { 12 | margin-top: 15px; 13 | } 14 | 15 | input.form-control { 16 | border-left-width: 5px; 17 | } 18 | 19 | .invalid-feedback { 20 | display: block; 21 | } 22 | 23 | .group-name { 24 | padding-top: 0; 25 | font-weight: 500; 26 | padding-right: 0; 27 | } 28 | 29 | a.group-name { 30 | color: inherit; 31 | text-decoration: none; 32 | align-self: start; 33 | } 34 | 35 | .permissionsColumn { 36 | margin-bottom: 20px; 37 | } 38 | 39 | .permissionsRow { 40 | margin: 0 15px; 41 | } 42 | 43 | 44 | .well-sm { 45 | padding: 0.5rem; 46 | } 47 | 48 | @media (min-width: 992px) { 49 | .user-enabled { 50 | margin-left: 40px; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/roles-management.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 7 |
8 |
9 | 18 |
19 |
20 | 21 | 30 | 31 | 32 | 33 | {{value}} 34 | 35 | 36 | 37 | 38 | @if (canManageRoles) { 39 | 40 | {{'roles.management.Edit' | translate}} 41 | 42 | | 43 | 44 | {{'roles.management.Delete' | translate}} 45 | 46 | } 47 | @else { 48 | 49 | {{'roles.management.Details' | translate}} 50 | 51 | } 52 | 53 | 54 | 55 | 69 | 72 | 73 |
74 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/roles-management.component.scss: -------------------------------------------------------------------------------- 1 |  2 | .control-box { 3 | margin-bottom: 5px; 4 | } 5 | 6 | .search-box { 7 | margin: 0; 8 | } 9 | 10 | .nav-item.toolbaritem a { 11 | padding-top: 3px; 12 | padding-bottom: 3px; 13 | min-width: 100px; 14 | } -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/search-box.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | @if (searchInput.value) { 5 | 6 | } 7 |
8 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/search-box.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .search-icon { 3 | pointer-events: none; 4 | } 5 | 6 | input[type=text]::-ms-clear { 7 | display: none; 8 | width: 0; 9 | height: 0; 10 | } 11 | 12 | input[type=text]::-ms-reveal { 13 | display: none; 14 | width: 0; 15 | height: 0; 16 | } 17 | 18 | input[type="search"]::-webkit-search-decoration, 19 | input[type="search"]::-webkit-search-cancel-button, 20 | input[type="search"]::-webkit-search-results-button, 21 | input[type="search"]::-webkit-search-results-decoration { 22 | display: none; 23 | } 24 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/search-box.component.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Component, ElementRef, HostListener, input, output, viewChild } from '@angular/core'; 8 | import { FormsModule } from '@angular/forms'; 9 | 10 | @Component({ 11 | selector: 'app-search-box', 12 | templateUrl: './search-box.component.html', 13 | styleUrl: './search-box.component.scss', 14 | imports: [FormsModule] 15 | }) 16 | export class SearchBoxComponent { 17 | readonly placeholder = input('Search...'); 18 | 19 | readonly searchChange = output(); 20 | 21 | readonly searchInput = viewChild.required('searchInput'); 22 | 23 | onValueChange(value: string) { 24 | setTimeout(() => this.searchChange.emit(value)); 25 | } 26 | 27 | @HostListener('keydown.escape') 28 | clear() { 29 | const searchInput = this.searchInput(); 30 | 31 | searchInput.nativeElement.value = ''; 32 | this.onValueChange(searchInput.nativeElement.value); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/statistics-demo.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 12 |
13 |
14 |
15 |
16 | 17 | 18 | 19 | 20 | @for (label of chartLabels; track label) { 21 | 22 | } 23 | 24 | 25 | 26 | @for (d of chartData; track d) { 27 | 28 | 29 | @for (label of chartLabels; track label; let j = $index) { 30 | 31 | } 32 | 33 | } 34 | 35 |
{{label}}
{{d && d.label.split(' ').pop()}}{{d && d.data[j]}}
36 |
37 | 38 |
39 | 40 | 49 |
50 | 51 | 52 |
53 |
54 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/statistics-demo.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .statistics-container { 3 | font-size: 0.875rem; 4 | } 5 | 6 | .chart-container { 7 | display: block; 8 | } 9 | 10 | .refresh-btn { 11 | margin-right: 10px; 12 | } 13 | 14 | table.bg-transparent > :not(caption) > * > * { 15 | background: transparent !important; 16 | } 17 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/todo-demo.component.scss: -------------------------------------------------------------------------------- 1 | input.form-control { 2 | border-left-width: 5px; 3 | } 4 | 5 | .control-box { 6 | margin-bottom: 5px; 7 | } 8 | 9 | .search-box { 10 | margin: 0; 11 | } 12 | 13 | .nav-item.toolbaritem a { 14 | padding-top: 3px; 15 | padding-bottom: 3px; 16 | min-width: 100px; 17 | font-weight: bold; 18 | } 19 | 20 | 21 | .completed { 22 | text-decoration: line-through; 23 | } 24 | 25 | .form-check { 26 | margin: 0; 27 | } 28 | 29 | 30 | .inline-label { 31 | width: 100%; 32 | min-height: 1rem; 33 | display: inline-block; 34 | } 35 | 36 | .inline-editor { 37 | width: 100%; 38 | } 39 | 40 | .description-form-group { 41 | margin-bottom: 5px; 42 | } 43 | 44 | .hr-separator { 45 | margin: 10px 0; 46 | } 47 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/user-info.component.scss: -------------------------------------------------------------------------------- 1 | .row:not(:last-child) { 2 | /*border-bottom: 1px solid #ccc;*/ 3 | } 4 | 5 | @mixin hr-dashed($margin: 10px 5px ) { 6 | margin: $margin; 7 | border-top: 1px dashed currentColor; 8 | background-color: inherit; 9 | } 10 | 11 | .info-separator { 12 | @include hr-dashed(0 5px); 13 | } 14 | 15 | .edit-separator { 16 | @include hr-dashed; 17 | } 18 | 19 | .hr-password-separator { 20 | margin: 4px; 21 | border-style: none; 22 | height: 0; 23 | } 24 | 25 | .last-separator { 26 | margin-top: 5px; 27 | } 28 | 29 | .last-edit-separator { 30 | margin-top: 15px; 31 | } 32 | 33 | input.form-control { 34 | border-left-width: 5px; 35 | } 36 | 37 | .invalid-feedback { 38 | display: block; 39 | } 40 | 41 | .password-well { 42 | padding: 0.5rem; 43 | } 44 | 45 | .hint-sm { 46 | display: block; 47 | } 48 | 49 | .form-check.user-enabled { 50 | display: inline-block; 51 | } 52 | 53 | .unblock-user { 54 | margin-left: 34px; 55 | } 56 | 57 | .send-confirm-email { 58 | padding: 0; 59 | vertical-align: inherit; 60 | font-size: 80%; 61 | } 62 | 63 | 64 | @media (min-width: 992px) { 65 | .user-enabled { 66 | margin-left: 40px; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/user-preferences.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .info-separator { 3 | margin: 7px 5px; 4 | border-top: 1px dashed currentColor; 5 | background-color: inherit; 6 | } 7 | 8 | .hr-sub-separator { 9 | margin: 4px 5px; 10 | border-top-style: none; 11 | height: 0; 12 | } 13 | 14 | .hr-last-separator { 15 | margin-top: 7px; 16 | } 17 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/users-management.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 7 |
8 |
9 | 18 |
19 |
20 | 21 | 30 | 31 | 32 | 33 | {{value}} 34 | 35 | 36 | 37 | 38 | @if (row.isLockedOut) { 39 | 40 | } 41 | @if (!row.isEnabled) { 42 | 43 | } 44 | {{value}} 45 | 46 | 47 | 48 | 49 |
50 | @for (role of value; track $index) { 51 | {{role}} 52 | } 53 |
54 |
55 | 56 | 57 | @if (canManageUsers) { 58 | 66 | } 67 | 68 | 69 | 70 | 79 | 82 | 83 |
84 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/controls/users-management.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .user-role { 3 | font-size: 0.7em !important; 4 | margin-right: 1px; 5 | } 6 | 7 | .control-box { 8 | margin-bottom: 5px; 9 | } 10 | 11 | .search-box { 12 | margin: 0; 13 | } 14 | 15 | .nav-item.toolbaritem a { 16 | padding-top: 3px; 17 | padding-bottom: 3px; 18 | min-width: 100px; 19 | } 20 | 21 | .user-disabled { 22 | color: #777; 23 | font-style: italic; 24 | } 25 | 26 | .locked-out { 27 | background-color: orangered; 28 | color: whitesmoke; 29 | width: 100%; 30 | display: inline-block; 31 | padding-left: 5px; 32 | } 33 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/customers/customers.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/customers/customers.component.scss: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /quickapp.client/src/app/components/customers/customers.component.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Component } from '@angular/core'; 8 | import { TranslateModule } from '@ngx-translate/core'; 9 | 10 | import { fadeInOut } from '../../services/animations'; 11 | import { TodoDemoComponent } from '../controls/todo-demo.component'; 12 | 13 | @Component({ 14 | selector: 'app-customers', 15 | templateUrl: './customers.component.html', 16 | styleUrl: './customers.component.scss', 17 | animations: [fadeInOut], 18 | imports: [TodoDemoComponent, TranslateModule] 19 | }) 20 | export class CustomersComponent { 21 | 22 | } 23 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/home/home.component.scss: -------------------------------------------------------------------------------- 1 | @import 'bootstrap/scss/functions'; 2 | @import 'bootstrap/scss/variables'; 3 | @import 'bootstrap/scss/mixins'; 4 | 5 | .widget-container { 6 | position: relative; 7 | border-radius: $border-radius; 8 | transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1); 9 | box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.18); 10 | cursor: move; 11 | 12 | .btn-close { 13 | position: absolute; 14 | top: 0.55rem; 15 | right: 1.3rem; 16 | font-size: 0.7rem; 17 | z-index: 2; 18 | 19 | &.extra-down { 20 | top: 3rem; 21 | } 22 | } 23 | 24 | &.no-shadow:not(:active):not(.cdk-drag-preview) { 25 | border: none; 26 | box-shadow: none; 27 | } 28 | 29 | &:active { 30 | box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12); 31 | 32 | &.no-shadow { 33 | border: solid 1px transparent; 34 | } 35 | } 36 | } 37 | 38 | .widget-container-placeholder { 39 | background: #cccccc7a; 40 | border: dashed 1px #999; 41 | min-height: 200px; 42 | transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); 43 | } 44 | 45 | .cdk-drag-preview { 46 | background: var(--bs-body-bg); 47 | box-sizing: border-box; 48 | border-radius: $border-radius; 49 | box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); 50 | } 51 | 52 | .cdk-drag-animating { 53 | transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); 54 | } 55 | 56 | .cdk-drop-list-dragging .widget-container:not(.cdk-drag-placeholder) { 57 | transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); 58 | } 59 | 60 | @include media-breakpoint-down(lg) { 61 | .widget-container .btn-close.extra-down { 62 | top: 4.9rem; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/login/login.component.scss: -------------------------------------------------------------------------------- 1 | .boxshadow { 2 | position: relative; 3 | /*box-shadow: 1px 2px 4px rgba(0, 0, 0, .5);*/ 4 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 5 | padding: 10px; 6 | background: white; 7 | } 8 | 9 | .boxshadow::after { 10 | content: ''; 11 | position: absolute; 12 | z-index: -1; /* hide shadow behind image */ 13 | box-shadow: 0 15px 20px rgba(0, 0, 0, 0.3); 14 | width: 70%; 15 | left: 15%; /* one half of the remaining 30% */ 16 | height: 100px; 17 | bottom: 0; 18 | } 19 | 20 | .card-header { 21 | padding: 0.75rem 1.25rem; 22 | 23 | .btn-close { 24 | height: 1rem; 25 | } 26 | } 27 | 28 | .last-control-group { 29 | margin-bottom: -0.75rem; 30 | } 31 | 32 | .h-90 { 33 | height: 90%; 34 | } 35 | 36 | 37 | @media (min-width: 768px) { 38 | .login-container { 39 | width: 670px; 40 | } 41 | } 42 | 43 | @media (min-width: 1200px) { 44 | .login-container { 45 | width: 700px; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/not-found/not-found.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 |
{{'notFound.404' | translate}}
8 |
{{'notFound.pageNotFound' | translate}}
9 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/not-found/not-found.component.scss: -------------------------------------------------------------------------------- 1 |  2 | .icon-container { 3 | font-size: 5rem; 4 | } 5 | 6 | .error-description { 7 | font-size: 1.5rem; 8 | padding-bottom: 10px; 9 | } 10 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/not-found/not-found.component.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Component } from '@angular/core'; 8 | import { RouterLink } from '@angular/router'; 9 | import { TranslateModule } from '@ngx-translate/core'; 10 | 11 | import { fadeInOut } from '../../services/animations'; 12 | 13 | @Component({ 14 | selector: 'app-not-found', 15 | templateUrl: './not-found.component.html', 16 | styleUrl: './not-found.component.scss', 17 | animations: [fadeInOut], 18 | imports: [RouterLink, TranslateModule] 19 | }) 20 | export class NotFoundComponent { 21 | } 22 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/orders/orders.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | -- Sample Page -- 7 |
8 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/orders/orders.component.scss: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /quickapp.client/src/app/components/orders/orders.component.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Component } from '@angular/core'; 8 | import { TranslateModule } from '@ngx-translate/core'; 9 | 10 | import { fadeInOut } from '../../services/animations'; 11 | 12 | @Component({ 13 | selector: 'app-orders', 14 | templateUrl: './orders.component.html', 15 | styleUrl: './orders.component.scss', 16 | animations: [fadeInOut], 17 | imports: [TranslateModule] 18 | }) 19 | export class OrdersComponent { 20 | } 21 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/products/products.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | -- Sample Page -- 7 |
8 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/products/products.component.scss: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /quickapp.client/src/app/components/products/products.component.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Component } from '@angular/core'; 8 | import { TranslateModule } from '@ngx-translate/core'; 9 | 10 | import { fadeInOut } from '../../services/animations'; 11 | 12 | @Component({ 13 | selector: 'app-products', 14 | templateUrl: './products.component.html', 15 | styleUrl: './products.component.scss', 16 | animations: [fadeInOut], 17 | imports: [TranslateModule] 18 | }) 19 | export class ProductsComponent { 20 | } 21 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/settings/settings.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 | 67 |
68 |
69 |
70 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/settings/settings.component.scss: -------------------------------------------------------------------------------- 1 | .hr-separator { 2 | margin-top: 0; 3 | margin-bottom: 10px; 4 | } 5 | -------------------------------------------------------------------------------- /quickapp.client/src/app/components/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Component, inject, OnInit, OnDestroy, AfterViewInit } from '@angular/core'; 8 | import { ActivatedRoute, Router, RouterLink } from '@angular/router'; 9 | import { TranslateModule } from '@ngx-translate/core'; 10 | import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; 11 | import { Subscription } from 'rxjs'; 12 | 13 | import { fadeInOut } from '../../services/animations'; 14 | import { AccountService } from '../../services/account.service'; 15 | import { Permissions } from '../../models/permission.model'; 16 | import { UserInfoComponent } from '../controls/user-info.component'; 17 | import { UserPreferencesComponent } from '../controls/user-preferences.component'; 18 | import { UsersManagementComponent } from '../controls/users-management.component'; 19 | import { RolesManagementComponent } from '../controls/roles-management.component'; 20 | 21 | @Component({ 22 | selector: 'app-settings', 23 | templateUrl: './settings.component.html', 24 | styleUrl: './settings.component.scss', 25 | animations: [fadeInOut], 26 | imports: [ 27 | RouterLink, TranslateModule, NgbNavModule, 28 | UserInfoComponent, UserPreferencesComponent, UsersManagementComponent, RolesManagementComponent 29 | ] 30 | }) 31 | export class SettingsComponent implements OnInit, AfterViewInit, OnDestroy { 32 | private router = inject(Router); 33 | public route = inject(ActivatedRoute); 34 | private accountService = inject(AccountService); 35 | 36 | readonly profileTab = 'profile'; 37 | readonly preferencesTab = 'preferences'; 38 | readonly usersTab = 'users'; 39 | readonly rolesTab = 'roles'; 40 | activeTab = ''; 41 | showDatatable = false; // Delays showing the table until tab is shown so column widths are calculated correctly 42 | fragmentSubscription: Subscription | undefined; 43 | 44 | ngOnInit() { 45 | this.fragmentSubscription = this.route.fragment.subscribe(fragment => this.setActiveTab(fragment)); 46 | } 47 | 48 | ngAfterViewInit() { 49 | setTimeout(() => this.showDatatable = true); 50 | } 51 | 52 | ngOnDestroy() { 53 | this.fragmentSubscription?.unsubscribe(); 54 | } 55 | 56 | setActiveTab(fragment: string | null) { 57 | fragment = fragment?.toLowerCase() ?? this.profileTab; 58 | 59 | const canViewTab = fragment === this.profileTab || fragment === this.preferencesTab || 60 | (fragment === this.usersTab && this.canViewUsers) || (fragment === this.rolesTab && this.canViewRoles); 61 | 62 | if (canViewTab) 63 | this.activeTab = fragment; 64 | else 65 | this.router.navigate([], { fragment: this.profileTab }); 66 | } 67 | 68 | get canViewUsers() { 69 | return this.accountService.userHasPermission(Permissions.viewUsers); 70 | } 71 | 72 | get canViewRoles() { 73 | return this.accountService.userHasPermission(Permissions.viewRoles); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /quickapp.client/src/app/directives/autofocus.directive.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Directive, ElementRef, OnInit, inject } from '@angular/core'; 8 | 9 | @Directive({ 10 | selector: '[appAutofocus]', 11 | standalone: true 12 | }) 13 | export class AutofocusDirective implements OnInit { 14 | elementRef = inject(ElementRef); 15 | 16 | ngOnInit() { 17 | setTimeout(() => this.elementRef.nativeElement.focus(), 500); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /quickapp.client/src/app/directives/equal-validator.directive.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Directive, forwardRef, HostAttributeToken, inject } from '@angular/core'; 8 | import { Validator, AbstractControl, NG_VALIDATORS, ValidationErrors } from '@angular/forms'; 9 | 10 | @Directive({ 11 | selector: '[appValidateEqual][formControlName],[appValidateEqual][formControl],[appValidateEqual][ngModel]', 12 | providers: [ 13 | { provide: NG_VALIDATORS, useExisting: forwardRef(() => EqualValidator), multi: true } 14 | ], 15 | standalone: true 16 | }) 17 | export class EqualValidator implements Validator { 18 | validateEqual = inject(new HostAttributeToken('appValidateEqual')); 19 | reverse = inject(new HostAttributeToken('reverse'), { optional: true }); 20 | 21 | validate(control: AbstractControl): ValidationErrors | null { 22 | const other = control.root.get(this.validateEqual); 23 | 24 | if (!other) { 25 | return null; 26 | } 27 | 28 | return this.reverse === 'true' ? this.validateReverse(control, other) : this.validateNoReverse(control, other); 29 | } 30 | 31 | private validateNoReverse(control: AbstractControl, other: AbstractControl): ValidationErrors | null { 32 | return other.value === control.value ? null : { validateEqual: true }; 33 | } 34 | 35 | private validateReverse(control: AbstractControl, other: AbstractControl): ValidationErrors | null { 36 | if (control.value === other.value) { 37 | if (other.errors) { 38 | delete other.errors['validateEqual']; 39 | 40 | if (Object.keys(other.errors).length === 0) { 41 | other.setErrors(null); 42 | } 43 | } 44 | } else { 45 | other.setErrors({ validateEqual: true }); 46 | } 47 | 48 | return null; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /quickapp.client/src/app/models/app-theme.model.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | export interface AppTheme { 8 | id: number; 9 | name: string; 10 | href: string; 11 | isDefault?: boolean; 12 | background: string; 13 | color: string; 14 | isDark?: boolean; 15 | } 16 | -------------------------------------------------------------------------------- /quickapp.client/src/app/models/enums.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | export enum Gender { 8 | None, 9 | Female, 10 | Male 11 | } 12 | -------------------------------------------------------------------------------- /quickapp.client/src/app/models/environment.model.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | export interface Environment { 8 | production: boolean; 9 | baseUrl?: string | null; 10 | fallbackBaseUrl?: string | null; 11 | } 12 | -------------------------------------------------------------------------------- /quickapp.client/src/app/models/login-response.model.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { PermissionValues } from './permission.model'; 8 | 9 | 10 | export interface LoginResponse { 11 | id_token: string; 12 | access_token: string; 13 | refresh_token: string; 14 | expires_in: number; 15 | token_type: string; 16 | scope: string; 17 | } 18 | 19 | 20 | export interface IdToken { 21 | iat: number; 22 | exp: number; 23 | iss: string; 24 | aud: string | string[]; 25 | sub: string; 26 | role: string | string[]; 27 | permission: PermissionValues | PermissionValues[]; 28 | name: string; 29 | email: string; 30 | phone_number: string; 31 | fullname: string; 32 | jobtitle: string; 33 | configuration: string; 34 | } 35 | -------------------------------------------------------------------------------- /quickapp.client/src/app/models/notification.model.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Utilities } from '../services/utilities'; 8 | 9 | 10 | export class Notification { 11 | public id = 0; 12 | public header = ''; 13 | public body = ''; 14 | public date = new Date(); 15 | public isRead = false; 16 | public isPinned = false; 17 | 18 | public static Create(data: object) { 19 | const n = new Notification(); 20 | Object.assign(n, data); 21 | 22 | if (n.date) { 23 | n.date = Utilities.parseDate(n.date); 24 | } 25 | 26 | return n; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /quickapp.client/src/app/models/permission.model.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | export type PermissionNames = 8 | 'View Users' | 'Manage Users' | 9 | 'View Roles' | 'Manage Roles' | 'Assign Roles'; 10 | 11 | export type PermissionValues = 12 | 'users.view' | 'users.manage' | 13 | 'roles.view' | 'roles.manage' | 'roles.assign'; 14 | 15 | export interface Permission { 16 | name: PermissionNames; 17 | value: PermissionValues; 18 | groupName: string; 19 | description: string; 20 | } 21 | 22 | export class Permissions { 23 | public static readonly viewUsers: PermissionValues = 'users.view'; 24 | public static readonly manageUsers: PermissionValues = 'users.manage'; 25 | 26 | public static readonly viewRoles: PermissionValues = 'roles.view'; 27 | public static readonly manageRoles: PermissionValues = 'roles.manage'; 28 | public static readonly assignRoles: PermissionValues = 'roles.assign'; 29 | } 30 | -------------------------------------------------------------------------------- /quickapp.client/src/app/models/role.model.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Permission } from './permission.model'; 8 | 9 | export class Role { 10 | constructor( 11 | public name = '', 12 | public description = '', 13 | public permissions: Permission[] = [] 14 | ) { } 15 | 16 | public id = ''; 17 | public usersCount = 0; 18 | } 19 | -------------------------------------------------------------------------------- /quickapp.client/src/app/models/user-edit.model.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { User } from './user.model'; 8 | 9 | export class UserEdit extends User { 10 | constructor( 11 | public currentPassword?: string, 12 | public newPassword?: string, 13 | public confirmPassword?: string) { 14 | super(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /quickapp.client/src/app/models/user-login.model.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | export class UserLogin { 8 | constructor( 9 | public userName = '', 10 | public password = '', 11 | public rememberMe?: boolean 12 | ) { } 13 | } 14 | -------------------------------------------------------------------------------- /quickapp.client/src/app/models/user.model.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | export class User { 8 | constructor( 9 | public id = '', 10 | public userName = '', 11 | public fullName = '', 12 | public email = '', 13 | public jobTitle = '', 14 | public phoneNumber = '', 15 | roles: string[] = [] 16 | ) { 17 | if (roles) 18 | this.roles = roles; 19 | } 20 | 21 | get friendlyName() { 22 | let name = this.fullName || this.userName; 23 | 24 | if (this.jobTitle) { 25 | name = this.jobTitle + ' ' + name; 26 | } 27 | 28 | return name; 29 | } 30 | 31 | public isEnabled = true; 32 | public isLockedOut = false; 33 | public roles: string[] = []; 34 | } 35 | -------------------------------------------------------------------------------- /quickapp.client/src/app/pipes/group-by.pipe.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Pipe, PipeTransform } from '@angular/core'; 8 | 9 | @Pipe({ 10 | name: 'groupBy', 11 | standalone: true 12 | }) 13 | export class GroupByPipe implements PipeTransform { 14 | transform(collection: T[], property: keyof T) { 15 | if (!collection) { 16 | return null; 17 | } 18 | 19 | const groupedCollection = collection.reduce((previous, current) => { 20 | const groupKey = current[property] as string; 21 | 22 | if (!previous[groupKey]) { 23 | previous[groupKey] = [current]; 24 | } else { 25 | previous[groupKey].push(current); 26 | } 27 | 28 | return previous; 29 | }, {} as Record); 30 | 31 | return Object.keys(groupedCollection) 32 | .map(key => ({ key, value: groupedCollection[key] })); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /quickapp.client/src/app/services/animations.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { animate, state, style, transition, trigger } from '@angular/animations'; 8 | 9 | 10 | export const fadeInOut = trigger('fadeInOut', [ 11 | transition(':enter', [style({ opacity: 0 }), animate('0.4s ease-in', style({ opacity: 1 }))]), 12 | transition(':leave', [animate('0.4s 10ms ease-out', style({ opacity: 0 }))]) 13 | ]); 14 | 15 | 16 | export function flyInOut(duration = 0.2) { 17 | return trigger('flyInOut', [ 18 | state('in', style({ opacity: 1, transform: 'translateX(0)' })), 19 | transition('void => *', [style({ opacity: 0, transform: 'translateX(-100%)' }), animate(`${duration}s ease-in`)]), 20 | transition('* => void', [animate(`${duration}s 10ms ease-out`, style({ opacity: 0, transform: 'translateX(100%)' }))]) 21 | ]); 22 | } 23 | -------------------------------------------------------------------------------- /quickapp.client/src/app/services/app-title.service.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Injectable, inject } from '@angular/core'; 8 | import { TitleStrategy, RouterStateSnapshot } from '@angular/router'; 9 | import { Title } from '@angular/platform-browser'; 10 | 11 | import { Utilities } from './utilities'; 12 | 13 | @Injectable() 14 | export class AppTitleService extends TitleStrategy { 15 | private readonly titleService = inject(Title); 16 | 17 | static appName: string | undefined; 18 | 19 | override updateTitle(routerState: RouterStateSnapshot) { 20 | let title = this.buildTitle(routerState); 21 | 22 | if (title) { 23 | const fragment = routerState.url.split('#')[1]; 24 | 25 | if (fragment) { 26 | title += ` | ${Utilities.toTitleCase(fragment)}`; 27 | } 28 | 29 | if (AppTitleService.appName) { 30 | title += ` - ${AppTitleService.appName}`; 31 | } 32 | 33 | this.titleService.setTitle(title); 34 | 35 | } else if (AppTitleService.appName) { 36 | this.titleService.setTitle(AppTitleService.appName); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /quickapp.client/src/app/services/app-translation.service.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { inject, Injectable } from '@angular/core'; 8 | import { HttpClient } from '@angular/common/http'; 9 | import { TranslateService, TranslateLoader } from '@ngx-translate/core'; 10 | import { of } from 'rxjs'; 11 | 12 | import fallbackLangData from '../../../public/locale/en.json'; 13 | 14 | @Injectable({ 15 | providedIn: 'root' 16 | }) 17 | export class AppTranslationService { 18 | private translate = inject(TranslateService); 19 | 20 | languageChanged$ = this.translate.onLangChange.asObservable(); 21 | 22 | constructor() { 23 | this.addLanguages(['en', 'fr', 'de', 'es', 'pt', 'zh', 'ko', 'ar']); 24 | this.setDefaultLanguage('en'); 25 | } 26 | 27 | addLanguages(lang: string[]) { 28 | this.translate.addLangs(lang); 29 | } 30 | 31 | setDefaultLanguage(lang: string) { 32 | this.translate.setDefaultLang(lang); 33 | } 34 | 35 | getDefaultLanguage() { 36 | return this.translate.defaultLang; 37 | } 38 | 39 | getBrowserLanguage() { 40 | return this.translate.getBrowserLang(); 41 | } 42 | 43 | getCurrentLanguage() { 44 | return this.translate.currentLang; 45 | } 46 | 47 | getLoadedLanguages() { 48 | return this.translate.langs; 49 | } 50 | 51 | useBrowserLanguage(): string | void { 52 | const browserLang = this.getBrowserLanguage(); 53 | 54 | if (browserLang?.match(/en|fr|de|es|pt|zh|ko|ar/)) { 55 | this.changeLanguage(browserLang); 56 | return browserLang; 57 | } 58 | } 59 | 60 | useDefaultLanguage() { 61 | return this.changeLanguage(null); 62 | } 63 | 64 | changeLanguage(language: string | null) { 65 | if (!language) { 66 | language = this.getDefaultLanguage(); 67 | } 68 | 69 | setTimeout(() => { this.translate.use(language); }); 70 | 71 | return language; 72 | } 73 | 74 | getTranslation(key: string | string[], interpolateParams?: object) { 75 | return this.translate.instant(key, interpolateParams); 76 | } 77 | 78 | getTranslationAsync(key: string | string[], interpolateParams?: object) { 79 | return this.translate.get(key, interpolateParams); 80 | } 81 | } 82 | 83 | 84 | export class TranslateLanguageLoader implements TranslateLoader { 85 | http = inject(HttpClient); 86 | 87 | public getTranslation(lang: string) { 88 | if (lang === 'en') 89 | return of(fallbackLangData); 90 | 91 | return this.http.get(`/locale/${lang}.json`); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /quickapp.client/src/app/services/auth-guard.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { inject } from '@angular/core'; 8 | import { CanActivateFn, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 9 | import { AuthService } from './auth.service'; 10 | 11 | export const AuthGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { 12 | const authService = inject(AuthService); 13 | 14 | if (authService.isLoggedIn) { 15 | return true; 16 | } 17 | 18 | authService.loginRedirectUrl = state.url; 19 | inject(Router).navigate(['/login']); 20 | 21 | return false; 22 | } 23 | -------------------------------------------------------------------------------- /quickapp.client/src/app/services/db-keys.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Injectable } from '@angular/core'; 8 | 9 | @Injectable() 10 | export class DBkeys { 11 | public static readonly CURRENT_USER = 'current_user'; 12 | public static readonly USER_PERMISSIONS = 'user_permissions'; 13 | public static readonly ACCESS_TOKEN = 'access_token'; 14 | public static readonly REFRESH_TOKEN = 'refresh_token'; 15 | public static readonly TOKEN_EXPIRES_IN = 'expires_in'; 16 | 17 | public static readonly REMEMBER_ME = 'remember_me'; 18 | 19 | public static readonly LANGUAGE = 'language'; 20 | public static readonly HOME_URL = 'home_url'; 21 | public static readonly THEME_ID = 'themeId'; 22 | public static readonly SHOW_DASHBOARD_STATISTICS = 'show_dashboard_statistics'; 23 | public static readonly SHOW_DASHBOARD_NOTIFICATIONS = 'show_dashboard_notifications'; 24 | public static readonly SHOW_DASHBOARD_TODO = 'show_dashboard_todo'; 25 | public static readonly SHOW_DASHBOARD_BANNER = 'show_dashboard_banner'; 26 | 27 | public static readonly USER_CONFIG_KEYS = 'user_config_keys'; 28 | } 29 | -------------------------------------------------------------------------------- /quickapp.client/src/app/services/jwt-helper.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | /** 8 | * Helper class to decode and find JWT expiration. 9 | */ 10 | import { Injectable } from '@angular/core'; 11 | 12 | 13 | @Injectable() 14 | export class JwtHelper { 15 | 16 | public urlBase64Decode(str: string): string { 17 | let output = str.replace(/-/g, '+').replace(/_/g, '/'); 18 | switch (output.length % 4) { 19 | case 0: { break; } 20 | case 2: { output += '=='; break; } 21 | case 3: { output += '='; break; } 22 | default: { 23 | throw new Error('Illegal base64url string!'); 24 | } 25 | } 26 | return this.b64DecodeUnicode(output); 27 | } 28 | 29 | // https://developer.mozilla.org/en/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem 30 | private b64DecodeUnicode(str: string) { 31 | return decodeURIComponent(Array.prototype.map.call(atob(str), c => { 32 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); 33 | }).join('')); 34 | } 35 | 36 | public decodeToken(token: string) { 37 | const parts = token.split('.'); 38 | 39 | if (parts.length !== 3) { 40 | throw new Error('JWT must have 3 parts'); 41 | } 42 | 43 | const decoded = this.urlBase64Decode(parts[1]); 44 | if (!decoded) { 45 | throw new Error('Cannot decode the token'); 46 | } 47 | 48 | return JSON.parse(decoded); 49 | } 50 | 51 | public getTokenExpirationDate(token: string): Date | null { 52 | const decoded = this.decodeToken(token); 53 | 54 | if (!Object.prototype.hasOwnProperty.call(decoded, 'exp')) { 55 | return null; 56 | } 57 | 58 | const date = new Date(0); // The 0 here is the key, which sets the date to the epoch 59 | date.setUTCSeconds(decoded.exp); 60 | 61 | return date; 62 | } 63 | 64 | public isTokenExpired(token: string, offsetSeconds?: number): boolean { 65 | const date = this.getTokenExpirationDate(token); 66 | offsetSeconds = offsetSeconds || 0; 67 | 68 | if (date == null) { 69 | return false; 70 | } 71 | 72 | // Token expired? 73 | return !(date.valueOf() > (new Date().valueOf() + (offsetSeconds * 1000))); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /quickapp.client/src/app/services/lowercase-url-serializer.service.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Injectable } from "@angular/core"; 8 | import { DefaultUrlSerializer, UrlTree } from "@angular/router"; 9 | import { Utilities } from "./utilities"; 10 | 11 | @Injectable() 12 | export class LowerCaseUrlSerializer extends DefaultUrlSerializer { 13 | override parse(url: string): UrlTree { 14 | const possibleSeparators = /[?;#]/; 15 | const indexOfSeparator = url.search(possibleSeparators); 16 | let processedUrl: string; 17 | 18 | if (indexOfSeparator > -1) { 19 | const separator = url.charAt(indexOfSeparator); 20 | const urlParts = Utilities.splitInTwo(url, separator); 21 | urlParts.firstPart = urlParts.firstPart.toLowerCase(); 22 | 23 | processedUrl = urlParts.firstPart + separator + urlParts.secondPart; 24 | } else { 25 | processedUrl = url.toLowerCase(); 26 | } 27 | 28 | return super.parse(processedUrl); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /quickapp.client/src/app/services/oidc-helper.service.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Injectable, inject } from '@angular/core'; 8 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; 9 | 10 | import { LocalStoreManager } from './local-store-manager.service'; 11 | import { ConfigurationService } from './configuration.service'; 12 | import { DBkeys } from './db-keys'; 13 | import { LoginResponse } from '../models/login-response.model'; 14 | 15 | @Injectable({ 16 | providedIn: 'root' 17 | }) 18 | export class OidcHelperService { 19 | private http = inject(HttpClient); 20 | private localStorage = inject(LocalStoreManager); 21 | private configurations = inject(ConfigurationService); 22 | 23 | private readonly clientId = 'quickapp_spa'; 24 | private readonly scope = 'openid email phone profile offline_access roles'; 25 | 26 | private get tokenEndpoint() { return `${this.configurations.baseUrl}/connect/token`; } 27 | 28 | loginWithPassword(userName: string, password: string) { 29 | const header = new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }); 30 | const params = new HttpParams() 31 | .append('username', userName) 32 | .append('password', password) 33 | .append('client_id', this.clientId) 34 | .append('grant_type', 'password') 35 | .append('scope', this.scope); 36 | 37 | return this.http.post(this.tokenEndpoint, params, { headers: header }); 38 | } 39 | 40 | refreshLogin() { 41 | const header = new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }); 42 | const params = new HttpParams() 43 | .append('refresh_token', this.refreshToken ?? '') 44 | .append('client_id', this.clientId) 45 | .append('grant_type', 'refresh_token'); 46 | 47 | return this.http.post(this.tokenEndpoint, params, { headers: header }); 48 | } 49 | 50 | get accessToken(): string | null { 51 | return this.localStorage.getData(DBkeys.ACCESS_TOKEN); 52 | } 53 | 54 | get accessTokenExpiryDate(): Date | null { 55 | return this.localStorage.getDataObject(DBkeys.TOKEN_EXPIRES_IN, true); 56 | } 57 | 58 | get refreshToken(): string | null { 59 | return this.localStorage.getData(DBkeys.REFRESH_TOKEN); 60 | } 61 | 62 | get isSessionExpired(): boolean { 63 | if (this.accessTokenExpiryDate == null) { 64 | return true; 65 | } 66 | 67 | return this.accessTokenExpiryDate.valueOf() <= new Date().valueOf(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /quickapp.client/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Environment } from "../app/models/environment.model"; 8 | 9 | export const environment: Environment = { 10 | production: true, 11 | baseUrl: null, // Set to null to use the current host (i.e if the client app and server api are hosted together) 12 | }; 13 | -------------------------------------------------------------------------------- /quickapp.client/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | import { Environment } from "../app/models/environment.model"; 8 | 9 | // This file can be replaced during build by using the `fileReplacements` array. 10 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 11 | // The list of file replacements can be found in `angular.json`. 12 | 13 | export const environment: Environment = { 14 | production: false, 15 | baseUrl: 'https://localhost:7085', // API Server url 16 | fallbackBaseUrl: 'https://quickapp.azurewebsites.net', // Fallback API Server for development without local API server 17 | }; 18 | 19 | /* 20 | * For easier debugging in development mode, you can import the following file 21 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 22 | * 23 | * This import should be commented out in production mode because it will have a negative impact 24 | * on performance if an error is thrown. 25 | */ 26 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 27 | -------------------------------------------------------------------------------- /quickapp.client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Home Page - QuickApp 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

19 | App is Loading... 20 |

21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 |

32 | QUICK APPLICATION SYSTEM - quickApp © 33 | WWW.EBENMONNEY.COM 34 |

35 |
36 |
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /quickapp.client/src/main.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // --------------------------------------- 3 | // Email: quickapp@ebenmonney.com 4 | // Templates: www.ebenmonney.com/templates 5 | // (c) 2024 www.ebenmonney.com/mit-license 6 | // --------------------------------------- 7 | 8 | import { bootstrapApplication } from '@angular/platform-browser'; 9 | import { appConfig } from './app/app.config'; 10 | import { AppComponent } from './app/app.component'; 11 | 12 | bootstrapApplication(AppComponent, appConfig) 13 | .catch((err) => console.error(err)); 14 | -------------------------------------------------------------------------------- /quickapp.client/src/proxy.conf.js: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Email: quickapp@ebenmonney.com 3 | // Templates: www.ebenmonney.com/templates 4 | // (c) 2024 www.ebenmonney.com/mit-license 5 | // --------------------------------------- 6 | 7 | const { env } = require('process'); 8 | 9 | const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` : 10 | env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'https://localhost:7085'; 11 | 12 | const PROXY_CONFIG = [ 13 | { 14 | context: [ 15 | "/api", 16 | "/swagger", 17 | "/connect", 18 | "/oauth", 19 | "/.well-known" 20 | ], 21 | target, 22 | secure: false 23 | } 24 | ] 25 | 26 | module.exports = PROXY_CONFIG; 27 | -------------------------------------------------------------------------------- /quickapp.client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/app", 7 | "types": [ 8 | "node", 9 | "@angular/localize" 10 | ] 11 | }, 12 | "files": [ 13 | "src/main.ts" 14 | ], 15 | "include": [ 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /quickapp.client/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "compileOnSave": false, 5 | "compilerOptions": { 6 | "outDir": "./dist/out-tsc", 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "skipLibCheck": true, 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "bundler", 17 | "importHelpers": true, 18 | "target": "ES2022", 19 | "module": "ES2022" 20 | }, 21 | "angularCompilerOptions": { 22 | "enableI18nLegacyMessageIdFormat": false, 23 | "strictInjectionParameters": true, 24 | "strictInputAccessModifiers": true, 25 | "strictTemplates": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /quickapp.client/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/spec", 7 | "types": [ 8 | "jasmine", 9 | "node", 10 | "@angular/localize" 11 | ] 12 | }, 13 | "include": [ 14 | "src/**/*.spec.ts", 15 | "src/**/*.d.ts" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------