This commit is contained in:
honzapatCZ 2024-09-02 17:30:42 +02:00
commit 77a536ee79
18 changed files with 3729 additions and 0 deletions

59
AutoScan.cs Normal file
View File

@ -0,0 +1,59 @@
namespace NejCommon;
public interface IScopedService
{
}
public interface ISingletonService
{
}
public interface ITransientService
{
}
public interface IBackgroundService
{
}
public interface ISettings
{
string Path { get; }
}
public static class AutoScan
{
public static void RegisterServices(this IServiceCollection collection)
{
collection.Scan(scan => scan
// We start out with all types in the assembly of ITransientService
.FromAssemblyOf<ITransientService>()
// AddClasses starts out with all public, non-abstract types in this assembly.
// These types are then filtered by the delegate passed to the method.
// In this case, we filter out only the classes that are assignable to ITransientService.
.AddClasses(classes => classes.AssignableTo<ITransientService>())
// We then specify what type we want to register these classes as.
// In this case, we want to register the types as all of its implemented interfaces.
// So if a type implements 3 interfaces; A, B, C, we'd end up with three separate registrations.
.AsSelf()
// And lastly, we specify the lifetime of these registrations.
.WithTransientLifetime()
// Here we start again, with a new full set of classes from the assembly above.
// This time, filtering out only the classes assignable to IScopedService.
.AddClasses(classes => classes.AssignableTo<IScopedService>())
// Now, we just want to register these types as a single interface, IScopedService.
.AsSelf()
// And again, just specify the lifetime.
.WithScopedLifetime()
.AddClasses(classes => classes.AssignableTo<ISingletonService>())
.AsSelf()
.AsImplementedInterfaces()
.WithSingletonLifetime()
.AddClasses(classes => classes.AssignableTo<IBackgroundService>())
.AsSelf()
.AsImplementedInterfaces()
.WithSingletonLifetime()
);
}
}

View File

@ -0,0 +1,172 @@
using System.Linq.Expressions;
using System.Reflection;
using ApiSourceGeneratorHelper;
using AutoMapPropertyHelper;
using EntityFrameworkCore.Projectables;
using EntityFrameworkCore.Projectables.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NejAccountingAPI.Controllers;
using NejAccountingAPI.Models;
using NejCommon.Utils;
using Swashbuckle.AspNetCore.Annotations;
namespace NejCommon.Controllers
{
public abstract class AutoController<TType, TRequest, TResponse> : AutoController<Company, TType, TRequest, TResponse> where TType : class, new() where TRequest : IAutomappedAttribute<TType, TRequest>, new() where TResponse : IAutomappedAttribute<TType, TResponse>, new()
{
public AutoController(AppDbContext appDb, IServiceProvider providers) : base(appDb, providers)
{
}
}
public abstract class AutoController<TOwner, TType, TRequest, TResponse> : AutoController<TOwner, TType, TResponse, TRequest, TResponse, TResponse, TRequest, TResponse>
where TType : class, new()
where TRequest : IAutomappedAttribute<TType, TRequest>, new()
where TResponse : IAutomappedAttribute<TType, TResponse>, new()
{
public AutoController(AppDbContext appDb, IServiceProvider providers) : base(appDb, providers)
{
}
}
/// <summary>
///
/// </summary>
/// <typeparam name="TType">The underyling type</typeparam>
/// <typeparam name="TRequest">The request type</typeparam>
/// <typeparam name="TResponse">The response type</typeparam>
[ApiController]
public abstract class AutoController<TOwner, TType, TGetAllResponse, TCreateRequest, TCreateResponse, TGetResponse, TUpdateRequest, TUpdateResponse> : ControllerBase
where TType : class, new()
where TGetAllResponse : IAutomappedAttribute<TType, TGetAllResponse>, new()
where TCreateRequest : IAutomappedAttribute<TType, TCreateRequest>, new()
where TCreateResponse : IAutomappedAttribute<TType, TCreateResponse>, new()
where TGetResponse : IAutomappedAttribute<TType, TGetResponse>, new()
where TUpdateRequest : IAutomappedAttribute<TType, TUpdateRequest>, new()
where TUpdateResponse : IAutomappedAttribute<TType, TUpdateResponse>, new()
{
protected readonly AppDbContext db;
protected readonly IServiceProvider providers;
public AutoController(AppDbContext appDb, IServiceProvider providers) : base()
{
db = appDb;
this.providers = providers;
}
protected abstract IQueryable<TType> GetQuery(TOwner comp);
protected virtual TType AssociateWithParent(TType entity, TOwner comp)
{
if (typeof(TOwner) != typeof(Company))
{
throw new NotImplementedException("Special parent association not implemented");
}
var props = typeof(TType).GetProperty("Company");
if (props == null)
{
//not implemented
throw new NotImplementedException("Special parent association not implemented");
}
props.SetValue(entity, comp);
return entity;
}
protected virtual IQueryable<TType> ApplyDefaultOrdering(IQueryable<TType> query)
{
return query;
}
/// <summary>
/// Get all the <typeparamref name="TType"/>
/// </summary>
/// <response code="200">Success</response>
/// <response code="0">There was an error</response>
[HttpGet]
public virtual async Task<Results<BadRequest<Error>, Ok<PaginationResponse<TGetAllResponse>>>> GetAll(TOwner company, [FromQuery] Pagination pag)
{
var data = await ApplyDefaultOrdering(GetQuery(company)).AsNoTrackingWithIdentityResolution().ApplyPaginationRes<TType, TGetAllResponse>(providers, pag);
//Console.Writeline(data.Data);
return TypedResults.Ok(data);
}
/// <summary>
/// Creates the <typeparamref name="TType"/>
/// </summary>
/// <param name="body"></param>
/// <response code="201">Success</response>
/// <response code="0">There was an error</response>
[HttpPost]
public virtual async Task<Results<BadRequest<Error>, CreatedAtRoute<TCreateResponse>>> Create(TOwner company, [FromBody] TCreateRequest body)
{
var entity = db.Create<TType>();
entity = AssociateWithParent(entity, company);
await db.AddAsync(entity);
body.ApplyTo(providers, entity);
return await db.ApiSaveChangesAsyncCreate<TType, TCreateResponse>(providers, entity);
}
/// <summary>
/// Gets the <typeparamref name="TType"/>
/// </summary>
/// <param name="entity"></param>
/// <response code="200">Success</response>
/// <response code="0">There was an error</response>
[HttpGet]
[Route("{id}/")]
public virtual async Task<Results<NotFound, Ok<TGetResponse>>> Get([FromServices][ModelBinder(Name = "id")] TType entity)
{
var dat = new TGetResponse().ApplyFrom(providers, entity);
return TypedResults.Ok(dat);
}
/// <summary>
/// Delete company.
/// </summary>
/// <param name="entity"></param>
/// <response code="200">Success</response>
/// <response code="0">There was an error</response>
[HttpDelete]
[Route("{id}/")]
public virtual async Task<Results<BadRequest<Error>, Ok>> Delete([FromServices][ModelBinder(Name = "id")] TType entity)
{
db.Remove(entity!);
return await db.ApiSaveChangesAsync(TypedResults.Ok());
}
/// <summary>
/// Update company.
/// </summary>
/// <param name="entity"></param>
/// <param name="body"></param>
/// <response code="200">Success</response>
/// <response code="0">There was an error</response>
[HttpPut]
[Route("{id}/")]
public virtual async Task<Results<NotFound, BadRequest<Error>, Ok<TUpdateResponse>>> Update([FromServices][ModelBinder(Name = "id")] TType entity, [FromBody] TUpdateRequest body)
{/*
if(entity is InvoiceBase inv){
//Console.Writeline(inv.InvoiceItems.Count);
}*/
body.ApplyTo(providers, entity);
var dat = new TUpdateResponse().ApplyFrom(providers, entity);
var res = await db.ApiSaveChangesAsyncOk<TType, TUpdateResponse>(providers, entity);
//use the private constructor thru reflection
var ctor = typeof(Results<NotFound, BadRequest<Error>, Ok<TUpdateResponse>>).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)[0];
return (Results<NotFound, BadRequest<Error>, Ok<TUpdateResponse>>)ctor.Invoke(new object[] { res.Result });
}
}
}

View File

@ -0,0 +1,118 @@
using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace NejCommon.Controllers
{
public class IApiDescriptionVisibilitySchemaFilter : ISchemaFilter
{
List<OpenApiSchema> schemas = new List<OpenApiSchema>();
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
RemoveSchemas(context);
//Get the .net type of the schema
var type = context.Type;
//Get attributes that imlement IApiDescriptionVisibilityProvider interface
var attributes = type.GetCustomAttributes(typeof(IApiDescriptionVisibilityProvider), true).Select(a => a as IApiDescriptionVisibilityProvider);
//If there are no attributes, then return
if (!attributes.Any())
return;
var ignore = attributes.Any(a => a.IgnoreApi);
//If the schema is ignored, then remove it from the schema repository
if (!ignore)
return;
schemas.Add(schema);
RemoveSchemas(context);
}
public void RemoveSchemas(SchemaFilterContext context)
{
foreach (var schema in schemas)
{
var key = context.SchemaRepository.Schemas.FirstOrDefault(k => k.Value == schema).Key;
if (string.IsNullOrWhiteSpace(key))
continue;
//Console.WriteLine($"Removing schema {key}");
context.SchemaRepository.Schemas.Remove(key);
}
}
}
public class OrphanedFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
// Add all paths parameters, bodies, responses and query parameters to a list
var ops = swaggerDoc.Paths.SelectMany(p => p.Value.Operations).ToList();
var Parameters = ops
.SelectMany(o => o.Value.Parameters)
.Select(x => x.Schema).ToList();
var requst = ops
.Select(o => o.Value.RequestBody)
.Where(c => c != null)
.SelectMany(x => x.Content)
.Select(c => c.Value.Schema).ToList();
var res = ops
.SelectMany(o => o.Value.Responses)
.SelectMany(r => r.Value.Content)
.Select(c => c.Value.Schema).ToList();
var usedSchemas = Parameters.Union(requst).Union(res).Distinct();
/*
Console.WriteLine($"Used schemas: {string.Join(", ", swaggerDoc.Components.Parameters)}");
var usedSchemas = swaggerDoc.Components.Parameters.Select(x => x.Value.Schema).
Union(swaggerDoc.Components.RequestBodies.Select(x => x.Value.Content).SelectMany(x => x.Values).Select(x => x.Schema)).
Union(swaggerDoc.Components.Responses.Select(x => x.Value.Content).SelectMany(x => x.Values).Select(x => x.Schema)).
Distinct();
*/
var schemas = usedSchemas.ToList();
var currentSchemas = new Dictionary<string, OpenApiSchema>();
foreach(var sch in schemas){
ExpandSchemas(context.SchemaRepository, currentSchemas, sch);
}
//Console.WriteLine($"Used schemas: {string.Join(", ", currentSchemas.Keys)}");
var orphanedSchemas = swaggerDoc.Components.Schemas.Where(x => !currentSchemas.ContainsKey(x.Key)).ToDictionary(x => x.Key, x => x.Value);
//Console.WriteLine($"Unused schemas: {string.Join(", ", orphanedSchemas.Keys)}");
swaggerDoc.Components.Schemas = swaggerDoc.Components.Schemas.Where(x => currentSchemas.ContainsKey(x.Key)).ToDictionary(x => x.Key, x => x.Value);
}
public static void ExpandSchemas(SchemaRepository repo, Dictionary<string, OpenApiSchema> currentSchemas, OpenApiSchema schemaToExpand)
{
if(schemaToExpand == null)
return;
if(schemaToExpand.Type == "array")
schemaToExpand = schemaToExpand.Items;
var selfRef = schemaToExpand.Reference?.Id;
var properties = schemaToExpand.Properties.Values.ToList();
if(selfRef != null && !currentSchemas.ContainsKey(selfRef) && repo.Schemas.ContainsKey(selfRef))
{
var sch = repo.Schemas[selfRef];
currentSchemas.Add(selfRef, sch);
ExpandSchemas(repo, currentSchemas, sch);
}
foreach(var sch in properties)
{
ExpandSchemas(repo, currentSchemas, sch);
}
}
}
}

View File

@ -0,0 +1,62 @@
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using ClosedXML.Excel;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
namespace NejCommon.Controllers;
public static class Responses
{
public static FileStreamHttpResult RespondXlsx(this ControllerBase controller, XLWorkbook file, string fileName)
{
var stream = new MemoryStream();
file.SaveAs(stream);
stream.Seek(0, SeekOrigin.Begin);
controller.Response.Headers.Add("Content-Disposition", "inline; filename=" + fileName);
return TypedResults.File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName);
}
public static FileStreamHttpResult RespondXlsxTable<T>(this ControllerBase controller, IEnumerable<T> data, string name = "Data", string? sheetName = null)
{
if (sheetName is null)
{
//limit the name to 31 characters
sheetName = name.Length > 31 ? name.Substring(0, 31) : name;
}
var notebook = new XLWorkbook();
var sheet = notebook.Worksheets.Add(sheetName);
sheet.FirstCell().InsertTable(data);
sheet.Columns().AdjustToContents();
return controller.RespondXlsx(notebook, name + ".xlsx");
}
public static FileStreamHttpResult RespondXml<T>(this ControllerBase controller, T obj, string fileName = "Data.xml")
{
var stream = new MemoryStream();
//use utf8 encoding
var serializer = new XmlSerializer(typeof(T));
var settings = new XmlWriterSettings
{
Indent = true,
Encoding = Encoding.UTF8,
};
//Console.Writeline(stream.Length);
using (var writer = XmlWriter.Create(stream, settings))
{
serializer.Serialize(writer, obj);
//Console.Writeline(stream.Length);
stream.Seek(0, SeekOrigin.Begin);
controller.Response.Headers.Add("Content-Disposition", "inline; filename=" + fileName);
return TypedResults.File(stream, "text/xml", fileName);
}
}
}

View File

@ -0,0 +1,108 @@
using System.Diagnostics.Eventing.Reader;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace NejCommon.Controllers
{
public class TypedResultsMetadataProvider : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var responseType = context.MethodInfo.ReturnType;
//Console.WriteLine(context.MethodInfo.DeclaringType.Name);
//Console.WriteLine(context.MethodInfo.Name);
//Console.WriteLine(responseType);
var t = IsSubclassOfRawGeneric(typeof(Microsoft.AspNetCore.Http.HttpResults.Results<,>), responseType);
if (t == null)
{
return;
}
var parArg = t.GetGenericArguments();
if (operation.Responses.ContainsKey("200"))
operation.Responses.Remove("200");
foreach (var arg in parArg)
{
if (arg == typeof(NotFound))
{
operation.Responses.Add("404", new OpenApiResponse { Description = "Not found" });
}
else if (arg == typeof(Ok))
{
operation.Responses.Add("200", new OpenApiResponse { Description = "Success" });
}
else if (IsSubclassOfRawGeneric(typeof(Ok<>), arg) != null)
{
var okArg = IsSubclassOfRawGeneric(typeof(Ok<>), arg).GetGenericArguments()[0];
Console.WriteLine("Adding: " + okArg);
//get or generate the schema
var schema = context.SchemaGenerator.GenerateSchema(okArg, context.SchemaRepository);
operation.Responses.Add("200", new OpenApiResponse { Description = "Success", Content = { { "application/json", new OpenApiMediaType { Schema = schema } } } });
}
else if (arg == typeof(CreatedAtRoute))
{
operation.Responses.Add("201", new OpenApiResponse { Description = "Success" });
}
else if (IsSubclassOfRawGeneric(typeof(CreatedAtRoute<>), arg) != null)
{
if (operation.Responses.ContainsKey("201"))
operation.Responses.Remove("201");
var okArg = IsSubclassOfRawGeneric(typeof(CreatedAtRoute<>), arg).GetGenericArguments()[0];
Console.WriteLine("Adding: " + okArg);
//get or generate the schema
var schema = context.SchemaGenerator.GenerateSchema(okArg, context.SchemaRepository);
operation.Responses.Add("201", new OpenApiResponse { Description = "Success", Content = { { "application/json", new OpenApiMediaType { Schema = schema } } } });
}
else if (arg == typeof(BadRequest))
{
operation.Responses.Add("400", new OpenApiResponse { Description = "There was an error" });
}
else if (IsSubclassOfRawGeneric(typeof(BadRequest<>), arg) != null)
{
if (operation.Responses.ContainsKey("400"))
operation.Responses.Remove("400");
var okArg = IsSubclassOfRawGeneric(typeof(BadRequest<>), arg).GetGenericArguments()[0];
Console.WriteLine("Adding: " + okArg);
//get or generate the schema
var schema = context.SchemaGenerator.GenerateSchema(okArg, context.SchemaRepository);
operation.Responses.Add("400", new OpenApiResponse { Description = "There was an error", Content = { { "application/json", new OpenApiMediaType { Schema = schema } } } });
}
else if (arg == typeof(FileStreamHttpResult)){
operation.Responses.Add("200", new OpenApiResponse { Description = "Success", Content = { { "application/octet-stream", new OpenApiMediaType { Schema = new OpenApiSchema { Type = "string", Format = "binary" } } } } });
}
else
{
Console.WriteLine("Unknown type: " + arg);
}
}
}
static Type? IsSubclassOfRawGeneric(Type generic, Type toCheck)
{
while (toCheck != null && toCheck != typeof(object))
{
//if Task is used, we need to check the underlying type
var realTypeNoTask = toCheck.IsGenericType && toCheck.GetGenericTypeDefinition() == typeof(Task<>) ? toCheck.GetGenericArguments()[0] : toCheck;
var cur = realTypeNoTask.IsGenericType ? realTypeNoTask.GetGenericTypeDefinition() : realTypeNoTask;
//Console.WriteLine(cur);
if (generic == cur)
{
return realTypeNoTask;
}
toCheck = toCheck.BaseType;
}
return null;
}
}
}

38
Emails/EmailBase.cs Normal file
View File

@ -0,0 +1,38 @@
using BlazorTemplater;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Mvc;
using NejAccountingAPI.Services.Email;
using PuppeteerSharp;
using System.Runtime.CompilerServices;
namespace NejAccountingAPI.Emails
{
public abstract class EmailBase<T1, T2> : ComponentBase where T1 : EmailBase<T1, T2>
{
public static string _css = "";
public static string GetCss()
{
if (_css == "")
_css = File.ReadAllText("./NejCommon/Emails/wwwroot/output.css");
return _css;
}
[Parameter, EditorRequired]
public T2 Data { get; set; } = default!;
public static async Task<string> GetHTML(T2 dat)
{
string html = new ComponentRenderer<T1>().Set(x => x.Data, dat).Render();
var result = PreMailer.Net.PreMailer.MoveCssInline(html, css: GetCss());
return result.Html;
}
public virtual string GetSubject()
{
return "DEV: " + this.ToString();
}
}
}

View File

@ -0,0 +1,23 @@
@namespace NejAccountingAPI.Emails
@inherits EmailBase<GeneralMessage, GeneralMessage.GeneralMessageData>
@code {
public struct GeneralMessageData
{
public GeneralMessageData(string subject, string message)
{
Message = message;
Subject = subject;
}
public string Message;
public string Subject;
}
public override string GetSubject()
{
return base.GetSubject();
}
}

1
Emails/__Imports.razor Normal file
View File

@ -0,0 +1 @@
@namespace NejAccountingAPI.Emails

828
Emails/wwwroot/output.css Normal file
View File

@ -0,0 +1,828 @@
/*
! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
*/
html {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font family by default.
2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
.static {
position: static;
}
.ml-2 {
margin-left: 0.5rem;
}
.inline {
display: inline;
}
.flex {
display: flex;
}
.table {
display: table;
}
.h-full {
height: 100%;
}
.h-16 {
height: 4rem;
}
.h-48 {
height: 12rem;
}
.w-full {
width: 100%;
}
.w-16 {
width: 4rem;
}
.w-48 {
width: 12rem;
}
.flex-grow {
flex-grow: 1;
}
.flex-col {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.items-end {
align-items: flex-end;
}
.items-center {
align-items: center;
}
.justify-end {
justify-content: flex-end;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.self-end {
align-self: flex-end;
}
.rounded-md {
border-radius: 0.375rem;
}
.rounded {
border-radius: 0.25rem;
}
.border-4 {
border-width: 4px;
}
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(var(--primary) / var(--tw-bg-opacity));
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.px-10 {
padding-left: 2.5rem;
padding-right: 2.5rem;
}
.pt-2 {
padding-top: 0.5rem;
}
.pb-2 {
padding-bottom: 0.5rem;
}
.pl-8 {
padding-left: 2rem;
}
.pr-8 {
padding-right: 2rem;
}
.pr-4 {
padding-right: 1rem;
}
.pt-1 {
padding-top: 0.25rem;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-3xl {
font-size: 1.875rem;
line-height: 2.25rem;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.font-semibold {
font-weight: 600;
}
.font-bold {
font-weight: 700;
}
.text-secondary {
--tw-text-opacity: 1;
color: rgb(var(--secondary-text) / var(--tw-text-opacity));
}
.text-primary {
--tw-text-opacity: 1;
color: rgb(var(--primary-text) / var(--tw-text-opacity));
}
.filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
:root {
/*
--primary: #3d3d3d;
--secondary: #535353;
--trinary: #2c2c2c;
--primary-text: #ffffff;
--secondary-text: #a0a0a0;
--primary-invert: #f3f3f3;
--secondary-invert: #dbdbdb;
--trinary-invert: #ffffff;
--primary-invert-text: #3d3d3d;
--secondary-invert-text: #535353;
--accent: #00C800;
--accent-dark: #008f00;
--accent-light: #00ed00;
--accent2: #3080FF;
--accent2-dark: #225ab4;
--accent3: #804000;
--accent3-dark: #472400;
--accent4: #F8B02C;
--accent4-dark: #bd8724;
--accent5: #9E3086;
--accent5-dark: #6b215b;
*/
--primary: 61 61 61;
--secondary: 83 83 83;
--trinary: 44 44 44;
--primary-text: 255 255 255;
--secondary-text: 237 246 255;
--primary-invert: 243 243 243;
--secondary-invert: 219 219 219;
--trinary-invert: 255 255 255;
--primary-invert-text: 61 61 61;
--secondary-invert-text: 83 83 83;
--accent: 57 172 231;
--accent-dark: 7 132 181;
--accent-light: 153 204 255;
--accent2: 48 128 255;
--accent2-dark: 34 90 180;
--accent3: 128 64 0;
--accent3-dark: 71 36 0;
--accent4: 248 176 44;
--accent4-dark: 189 135 36;
--accent5: 158 48 134;
--accent5-dark: 107 33 91;
--primary-border: 61 61 61;
--secondary-border: 83 83 83;
--trinary-border: 44 44 44;
}
.light\-theme {
--primary: 243 243 243;
--secondary: 219 219 219;
--trinary: 255 255 255;
--primary-text: 61 61 61;
--secondary-text: 83 83 83;
--primary-invert: 61 61 61;
--secondary-invert: 83 83 83;
--trinary-invert: 44 44 44;
--primary-text-invert: 255 255 255;
--secondary-text-invert: 160 160 160;
}
body {
-webkit-tap-highlight-color: var(--accent);
}
::-webkit-scrollbar {
width: 0.5rem;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--secondary);
border-radius: 0.25rem;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-dark);
}

87
Emails/wwwroot/site.css Normal file
View File

@ -0,0 +1,87 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
:root {
/*
--primary: #3d3d3d;
--secondary: #535353;
--trinary: #2c2c2c;
--primary-text: #ffffff;
--secondary-text: #a0a0a0;
--primary-invert: #f3f3f3;
--secondary-invert: #dbdbdb;
--trinary-invert: #ffffff;
--primary-invert-text: #3d3d3d;
--secondary-invert-text: #535353;
--accent: #00C800;
--accent-dark: #008f00;
--accent-light: #00ed00;
--accent2: #3080FF;
--accent2-dark: #225ab4;
--accent3: #804000;
--accent3-dark: #472400;
--accent4: #F8B02C;
--accent4-dark: #bd8724;
--accent5: #9E3086;
--accent5-dark: #6b215b;
*/
--primary: 61 61 61;
--secondary: 83 83 83;
--trinary: 44 44 44;
--primary-text: 255 255 255;
--secondary-text: 237 246 255;
--primary-invert: 243 243 243;
--secondary-invert: 219 219 219;
--trinary-invert: 255 255 255;
--primary-invert-text: 61 61 61;
--secondary-invert-text: 83 83 83;
--accent: 57 172 231;
--accent-dark: 7 132 181;
--accent-light: 153 204 255;
--accent2: 48 128 255;
--accent2-dark: 34 90 180;
--accent3: 128 64 0;
--accent3-dark: 71 36 0;
--accent4: 248 176 44;
--accent4-dark: 189 135 36;
--accent5: 158 48 134;
--accent5-dark: 107 33 91;
--primary-border: 61 61 61;
--secondary-border: 83 83 83;
--trinary-border: 44 44 44;
}
.light\-theme {
--primary: 243 243 243;
--secondary: 219 219 219;
--trinary: 255 255 255;
--primary-text: 61 61 61;
--secondary-text: 83 83 83;
--primary-invert: 61 61 61;
--secondary-invert: 83 83 83;
--trinary-invert: 44 44 44;
--primary-text-invert: 255 255 255;
--secondary-text-invert: 160 160 160;
}
body {
-webkit-tap-highlight-color: var(--accent);
}
::-webkit-scrollbar {
width: 0.5rem;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--secondary);
border-radius: 0.25rem;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-dark);
}

1067
Models/Country.cs Normal file

File diff suppressed because it is too large Load Diff

631
Models/Currency.cs Normal file
View File

@ -0,0 +1,631 @@
namespace NejCommon.Models
{
/// <summary>
/// Gets or Sets Currency
/// </summary>
public enum Currency
{
/// <summary>
/// AED
/// </summary>
AED,
/// <summary>
/// AFN
/// </summary>
AFN,
/// <summary>
/// ALL
/// </summary>
ALL,
/// <summary>
/// AMD
/// </summary>
AMD,
/// <summary>
/// ANG
/// </summary>
ANG,
/// <summary>
/// AOA
/// </summary>
AOA,
/// <summary>
/// ARS
/// </summary>
ARS,
/// <summary>
/// AUD
/// </summary>
AUD,
/// <summary>
/// AWG
/// </summary>
AWG,
/// <summary>
/// AZN
/// </summary>
AZN,
/// <summary>
/// BAM
/// </summary>
BAM,
/// <summary>
/// BBD
/// </summary>
BBD,
/// <summary>
/// BDT
/// </summary>
BDT,
/// <summary>
/// BGN
/// </summary>
BGN,
/// <summary>
/// BHD
/// </summary>
BHD,
/// <summary>
/// BIF
/// </summary>
BIF,
/// <summary>
/// BMD
/// </summary>
BMD,
/// <summary>
/// BND
/// </summary>
BND,
/// <summary>
/// BOB
/// </summary>
BOB,
/// <summary>
/// BRL
/// </summary>
BRL,
/// <summary>
/// BSD
/// </summary>
BSD,
/// <summary>
/// BTN
/// </summary>
BTN,
/// <summary>
/// BWP
/// </summary>
BWP,
/// <summary>
/// BYN
/// </summary>
BYN,
/// <summary>
/// BZD
/// </summary>
BZD,
/// <summary>
/// CAD
/// </summary>
CAD,
/// <summary>
/// CDF
/// </summary>
CDF,
/// <summary>
/// CHF
/// </summary>
CHF,
/// <summary>
/// CLP
/// </summary>
CLP,
/// <summary>
/// CNY
/// </summary>
CNY,
/// <summary>
/// COP
/// </summary>
COP,
/// <summary>
/// CRC
/// </summary>
CRC,
/// <summary>
/// CUP
/// </summary>
CUP,
/// <summary>
/// CVE
/// </summary>
CVE,
/// <summary>
/// CZK
/// </summary>
CZK,
/// <summary>
/// DJF
/// </summary>
DJF,
/// <summary>
/// DKK
/// </summary>
DKK,
/// <summary>
/// DOP
/// </summary>
DOP,
/// <summary>
/// DZD
/// </summary>
DZD,
/// <summary>
/// EGP
/// </summary>
EGP,
/// <summary>
/// ERN
/// </summary>
ERN,
/// <summary>
/// ETB
/// </summary>
ETB,
/// <summary>
/// EUR
/// </summary>
EUR,
/// <summary>
/// FJD
/// </summary>
FJD,
/// <summary>
/// FKP
/// </summary>
FKP,
/// <summary>
/// GBP
/// </summary>
GBP,
/// <summary>
/// GEL
/// </summary>
GEL,
/// <summary>
/// GHS
/// </summary>
GHS,
/// <summary>
/// GIP
/// </summary>
GIP,
/// <summary>
/// GMD
/// </summary>
GMD,
/// <summary>
/// GNF
/// </summary>
GNF,
/// <summary>
/// GTQ
/// </summary>
GTQ,
/// <summary>
/// GYD
/// </summary>
GYD,
/// <summary>
/// HKD
/// </summary>
HKD,
/// <summary>
/// HNL
/// </summary>
HNL,
/// <summary>
/// HRK
/// </summary>
HRK,
/// <summary>
/// HTG
/// </summary>
HTG,
/// <summary>
/// HUF
/// </summary>
HUF,
/// <summary>
/// IDR
/// </summary>
IDR,
/// <summary>
/// ILS
/// </summary>
ILS,
/// <summary>
/// INR
/// </summary>
INR,
/// <summary>
/// IQD
/// </summary>
IQD,
/// <summary>
/// IRR
/// </summary>
IRR,
/// <summary>
/// ISK
/// </summary>
ISK,
/// <summary>
/// JMD
/// </summary>
JMD,
/// <summary>
/// JOD
/// </summary>
JOD,
/// <summary>
/// JPY
/// </summary>
JPY,
/// <summary>
/// KES
/// </summary>
KES,
/// <summary>
/// KGS
/// </summary>
KGS,
/// <summary>
/// KHR
/// </summary>
KHR,
/// <summary>
/// KMF
/// </summary>
KMF,
/// <summary>
/// KPW
/// </summary>
KPW,
/// <summary>
/// KRW
/// </summary>
KRW,
/// <summary>
/// KWD
/// </summary>
KWD,
/// <summary>
/// KYD
/// </summary>
KYD,
/// <summary>
/// KZT
/// </summary>
KZT,
/// <summary>
/// LAK
/// </summary>
LAK,
/// <summary>
/// LBP
/// </summary>
LBP,
/// <summary>
/// LKR
/// </summary>
LKR,
/// <summary>
/// LRD
/// </summary>
LRD,
/// <summary>
/// LSL
/// </summary>
LSL,
/// <summary>
/// LYD
/// </summary>
LYD,
/// <summary>
/// MAD
/// </summary>
MAD,
/// <summary>
/// MDL
/// </summary>
MDL,
/// <summary>
/// MGA
/// </summary>
MGA,
/// <summary>
/// MKD
/// </summary>
MKD,
/// <summary>
/// MMK
/// </summary>
MMK,
/// <summary>
/// MNT
/// </summary>
MNT,
/// <summary>
/// MOP
/// </summary>
MOP,
/// <summary>
/// MRU
/// </summary>
MRU,
/// <summary>
/// MUR
/// </summary>
MUR,
/// <summary>
/// MVR
/// </summary>
MVR,
/// <summary>
/// MWK
/// </summary>
MWK,
/// <summary>
/// MXN
/// </summary>
MXN,
/// <summary>
/// MYR
/// </summary>
MYR,
/// <summary>
/// MZN
/// </summary>
MZN,
/// <summary>
/// NAD
/// </summary>
NAD,
/// <summary>
/// NGN
/// </summary>
NGN,
/// <summary>
/// NIO
/// </summary>
NIO,
/// <summary>
/// NOK
/// </summary>
NOK,
/// <summary>
/// NPR
/// </summary>
NPR,
/// <summary>
/// NZD
/// </summary>
NZD,
/// <summary>
/// OMR
/// </summary>
OMR,
/// <summary>
/// PAB
/// </summary>
PAB,
/// <summary>
/// PEN
/// </summary>
PEN,
/// <summary>
/// PGK
/// </summary>
PGK,
/// <summary>
/// PHP
/// </summary>
PHP,
/// <summary>
/// PKR
/// </summary>
PKR,
/// <summary>
/// PLN
/// </summary>
PLN,
/// <summary>
/// PYG
/// </summary>
PYG,
/// <summary>
/// QAR
/// </summary>
QAR,
/// <summary>
/// RON
/// </summary>
RON,
/// <summary>
/// RSD
/// </summary>
RSD,
/// <summary>
/// RUB
/// </summary>
RUB,
/// <summary>
/// RWF
/// </summary>
RWF,
/// <summary>
/// SAR
/// </summary>
SAR,
/// <summary>
/// SBD
/// </summary>
SBD,
/// <summary>
/// SCR
/// </summary>
SCR,
/// <summary>
/// SDG
/// </summary>
SDG,
/// <summary>
/// SEK
/// </summary>
SEK,
/// <summary>
/// SGD
/// </summary>
SGD,
/// <summary>
/// SHP
/// </summary>
SHP,
/// <summary>
/// SLL
/// </summary>
SLL,
/// <summary>
/// SOS
/// </summary>
SOS,
/// <summary>
/// SRD
/// </summary>
SRD,
/// <summary>
/// SSP
/// </summary>
SSP,
/// <summary>
/// STD
/// </summary>
STD,
/// <summary>
/// SYP
/// </summary>
SYP,
/// <summary>
/// SZL
/// </summary>
SZL,
/// <summary>
/// THB
/// </summary>
THB,
/// <summary>
/// TJS
/// </summary>
TJS,
/// <summary>
/// TMT
/// </summary>
TMT,
/// <summary>
/// TND
/// </summary>
TND,
/// <summary>
/// TOP
/// </summary>
TOP,
/// <summary>
/// TRY
/// </summary>
TRY,
/// <summary>
/// TTD
/// </summary>
TTD,
/// <summary>
/// TWD
/// </summary>
TWD,
/// <summary>
/// TZS
/// </summary>
TZS,
/// <summary>
/// UAH
/// </summary>
UAH,
/// <summary>
/// UGX
/// </summary>
UGX,
/// <summary>
/// USD
/// </summary>
USD,
/// <summary>
/// UYU
/// </summary>
UYU,
/// <summary>
/// UZS
/// </summary>
UZS,
/// <summary>
/// VEF
/// </summary>
VEF,
/// <summary>
/// VND
/// </summary>
VND,
/// <summary>
/// VUV
/// </summary>
VUV,
/// <summary>
/// WST
/// </summary>
WST,
/// <summary>
/// XAF
/// </summary>
XAF,
/// <summary>
/// XCD
/// </summary>
XCD,
/// <summary>
/// XOF
/// </summary>
XOF,
/// <summary>
/// XPF
/// </summary>
XPF,
/// <summary>
/// YER
/// </summary>
YER,
/// <summary>
/// ZAR
/// </summary>
ZAR,
/// <summary>
/// ZMW
/// </summary>
ZMW,
/// <summary>
/// ZWL
/// </summary>
ZWL
}
}

28
Models/CurrencyValue.cs Normal file
View File

@ -0,0 +1,28 @@
namespace NejCommon.Models
{
/// <summary>
/// A value with a currency
/// </summary>
public class CurrencyValue
{
public CurrencyValue()
{
Currency = Currency.EUR;
Value = 0;
}
public CurrencyValue(Currency currency, decimal value)
{
Value = value;
Currency = currency;
}
public decimal Value { get; set; }
public Currency Currency { get; set; }
public override string ToString()
{
return $"{Value} {Currency}";
}
}
}

269
Models/ModelBinder.cs Normal file
View File

@ -0,0 +1,269 @@
using AngleSharp.Dom;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using NejAccountingAPI.Documents;
using NejAccountingAPI.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
namespace NejCommon.Models
{
public abstract class RouteValue
{
public string Name { get; }
public bool Primary { get; } = false;
public RouteValue(string name, bool primary = false)
{
Name = name;
Primary = primary;
}
}
public class RouteValue<TEntity> : RouteValue where TEntity : class
{
public Expression<Func<TEntity, string, bool>> Expression { get; }
public RouteValue(string name, Expression<Func<TEntity, string, bool>> expression) : base(name)
{
Expression = expression;
}
public RouteValue(string name, bool primary, Expression<Func<TEntity, string, bool>> expression) : base(name, primary)
{
Expression = expression;
}
}
public abstract class EntityBinder
{
}
public abstract class EntityBinder<TEntity> : EntityBinder<TEntity, string> where TEntity : class
{
public EntityBinder(AppDbContext db, RouteValue<TEntity>[] routeValues) : base(db, routeValues)
{
}
}
//id should be Guid, int, string, etc.
public abstract class EntityBinder<TEntity, TIdType> : EntityBinder, IModelBinder
where TEntity : class
where TIdType : IEquatable<TIdType>
{
private readonly AppDbContext _db;
private readonly RouteValue<TEntity>[] _routeValues;
public RouteValue<TEntity>[] RouteValues => _routeValues;
public EntityBinder(AppDbContext db, RouteValue<TEntity>[] routeValues)
{
_db = db;
_routeValues = routeValues;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
//get the binding parameter attributes
// Fetch the route values from the route data
var capturedRouteData = new Dictionary<string, object>();
foreach (var routeValue in _routeValues)
{
var name = routeValue.Name;
if (routeValue.Primary && !string.IsNullOrWhiteSpace(bindingContext.ModelName))
{
name = bindingContext.ModelName;
}
/*
Console.WriteLine("Route value: " + name);
Console.WriteLine();
Console.WriteLine(bindingContext.ActionContext.RouteData.Values[name]);
*/
var value = bindingContext.ValueProvider.GetValue(name).FirstOrDefault();//bindingContext.ActionContext.RouteData.Values[name];
if (value is null)
{
bindingContext.ModelState.AddModelError(name, "Route value not found");
}
else
{
capturedRouteData[routeValue.Name] = value;
}
}
// Check if all the required route values are present
if (!bindingContext.ModelState.IsValid)
{
bindingContext.Result = ModelBindingResult.Failed();
return;
}
// Find the entity with the specified route values
var query = _db.Set<TEntity>().AsQueryable();
foreach (var routeValue in _routeValues)
{
var value = capturedRouteData[routeValue.Name];
var par = Expression.Parameter(typeof(TEntity));
var exp = Expression.Invoke(routeValue.Expression, par, Expression.Constant((string)value));
var redExp = exp.Reduce();
var compiledExp = Expression.Lambda<Func<TEntity, bool>>(redExp, par);
query = query.Where(compiledExp);
}
var model = await query.FirstOrDefaultAsync();
if (model == null)
{
bindingContext.HttpContext.Response.StatusCode = 404;
bindingContext.ModelState.AddModelError(bindingContext.FieldName, "Not found in DB");
bindingContext.Result = ModelBindingResult.Failed();
return;
}
bindingContext.Result = ModelBindingResult.Success(model);
bindingContext.ValidationState[bindingContext.Result] = new ValidationStateEntry
{
SuppressValidation = true
};
}
}
public class EntityBinderOperationFilter : IOperationFilter
{
public object CreateDummyInstance(Type entityBinderType)
{
var constructors = entityBinderType.GetConstructors();
var constructor = constructors.OrderBy(x => x.GetParameters().Count()).First();
var constructorParameters = constructor.GetParameters();
var entityBinder = (EntityBinder)constructor.Invoke(constructorParameters.Select(x => (object?)null).ToArray());
return entityBinder;
}
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var actionDescriptor = context.ApiDescription.ActionDescriptor;
// Get the action parameters with a ModelBinder attribute
var modelBinderParameters = actionDescriptor.Parameters
.Where(p => p.BindingInfo?.BinderType != null && typeof(EntityBinder).IsAssignableFrom(p.BindingInfo.BinderType))
.Where(x => x != null)
.ToList();
if (modelBinderParameters.Count == 0)
{
return;
}
//Console.WriteLine("Applying EntityBinderOperationFilter to: " + actionDescriptor.DisplayName);
operation.Parameters = operation.Parameters.Where(p => !modelBinderParameters.Any(mp => mp.Name == p.Name)).ToList();
/*
// Get the EntityBinder RouteValues
foreach (var parameter in modelBinderParameters)
{
var entityBinderType = parameter.BindingInfo!.BinderType;
var routeValuesProperty = entityBinderType!.GetProperty("RouteValues");
if (routeValuesProperty == null)
{
continue;
}
var routeValues = (IEnumerable<RouteValue>?)routeValuesProperty.GetValue(CreateDummyInstance(entityBinderType));
if (routeValues == null)
{
continue;
}
foreach (var routeValue in routeValues)
{
operation.Parameters.Add(new OpenApiParameter
{
Name = routeValue.Name,
In = ParameterLocation.Path,
Required = true,
Schema = new OpenApiSchema
{
Type = "string"
}
});
}
}*/
// Exclude the EntityBinder parameter types from the document schemas
var schemaRep = context.SchemaRepository;
foreach (var parameter in modelBinderParameters)
{
var entityBinderType = parameter.BindingInfo?.BinderType;
// Get the EntityBinder generic type argument
var entityType = GetBinderEntityType(entityBinderType);
if (entityType == null)
{
Console.WriteLine("Couldn't find entityType of: " + entityBinderType);
continue;
}
if (schemaRep.Schemas.ContainsKey(entityType.Name))
{
//Console.WriteLine("Removing schema: " + entityType.Name);
schemaRep.Schemas.Remove(entityType.Name);
}
}
}
private Type? GetBinderEntityType(Type? binderType)
{
if (binderType == null)
return null;
if (binderType.IsGenericType && binderType.GetGenericTypeDefinition() == typeof(EntityBinder<>))
{
return binderType.GetTypeInfo().GetGenericArguments().FirstOrDefault();
}
return GetBinderEntityType(binderType.BaseType);
}
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public sealed class EntityAttribute<TEntityBinder, TEntity> : Attribute, IPropertyValidationFilter, IApiDescriptionVisibilityProvider, IBinderTypeProviderMetadata where TEntityBinder : EntityBinder<TEntity> where TEntity : class
{
/// <inheritdoc />
public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry)
{
return false;
}
/// <inheritdoc />
public Type? BinderType => typeof(TEntityBinder);
/// <inheritdoc />
public BindingSource? BindingSource => BindingSource.Custom;
public bool IgnoreApi => true;
}
}

View File

@ -0,0 +1,23 @@
using System.Net.Mail;
using System.Reflection;
using NejAccountingAPI.Emails;
namespace NejAccountingAPI.Services.Email;
public interface IEmailService
{
Task SendEmailAsync(string recipient, string subject, string message, List<Attachment>? attachments = null);
async Task SendDocumentAsync<T, T2>(string recipient, T2 data, List<Attachment>? attachments = null) where T : EmailBase<T, T2>
{
var func = typeof(T).GetMethod("GetHTML", BindingFlags.Static | BindingFlags.FlattenHierarchy | BindingFlags.Public);
var obj = Activator.CreateInstance<T>();
obj.Data = data;
var subject = obj.GetSubject();
//invoke the GetHTML function asynchronously and wait for the result
var html = await (Task<string>)func.Invoke(null, new object[] { data });
await SendEmailAsync(recipient, subject, html, attachments);
}
}

View File

@ -0,0 +1,78 @@
using System.Net.Mail;
using MailKit;
using MailKit.Net.Imap;
using MailKit.Net.Smtp;
using MimeKit;
namespace NejAccountingAPI.Services.Email;
public class SMTPService : IEmailService
{
public record SMTPSettings(string From, string Host, int Port, int ImapPort, string Username, string Password);
private readonly SMTPSettings _settings;
private readonly ILogger<SMTPService> Logger;
public SMTPService(ILogger<SMTPService> logger, SMTPSettings settings)
{
_settings = settings;
Logger = logger;
}
public async Task SendEmailAsync(string recipient, string subject, string message, List<Attachment>? attachments = null)
{
var mess = new MimeMessage();
mess.From.Add(MailboxAddress.Parse(_settings.From));
mess.To.Add(MailboxAddress.Parse(recipient));
mess.Subject = subject;
var bodyBuilder = new BodyBuilder();
bodyBuilder.HtmlBody = message;
bodyBuilder.TextBody = "";
foreach (var attachment in attachments ?? new List<Attachment>())
bodyBuilder.Attachments.Add(attachment.Name, attachment.ContentStream, ContentType.Parse(attachment.ContentType.ToString()));
mess.Body = bodyBuilder.ToMessageBody();
using (var client = new MailKit.Net.Smtp.SmtpClient())
{
await client.ConnectAsync(_settings.Host, _settings.Port);
// Note: only needed if the SMTP server requires authentication
await client.AuthenticateAsync(_settings.Username, _settings.Password);
await client.SendAsync(mess);
await client.DisconnectAsync(true);
using (var imap = new MailKit.Net.Imap.ImapClient())
{
await imap.ConnectAsync(_settings.Host, _settings.ImapPort); // or ConnectSSL for SSL
await imap.AuthenticateAsync(_settings.Username, _settings.Password);
//get the sent folder
IMailFolder sent = null;
if (imap.Capabilities.HasFlag(ImapCapabilities.SpecialUse))
sent = imap.GetFolder(SpecialFolder.Sent);
if (sent == null)
{
// get the default personal namespace root folder
var personal = imap.GetFolder(imap.PersonalNamespaces[0]);
// This assumes the sent folder's name is "Sent", but use whatever the real name is
sent = await personal.GetSubfolderAsync("Sent").ConfigureAwait(false);
}
if (sent != null)
{
await sent.AppendAsync(mess, MessageFlags.Seen);
}
await imap.DisconnectAsync(true);
}
Logger.LogInformation("Email {0} sent to {1} - {2} attachments", subject, recipient, attachments?.Count ?? 0);
}
}
}

View File

@ -0,0 +1,18 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace NejCommon.Utils
{
public class DateOnlyConverter : JsonConverter<DateOnly>
{
public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return DateOnly.Parse(reader.GetString() ?? string.Empty);
}
public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("yyyy-MM-dd"));
}
}
}

119
Utils/Extensions.cs Normal file
View File

@ -0,0 +1,119 @@
using System.Globalization;
using System.Linq.Expressions;
using AutoMapPropertyHelper;
using EntityFrameworkCore.Projectables;
using EntityFrameworkCore.Projectables.Extensions;
using Microsoft.EntityFrameworkCore;
using NejAccountingAPI.Models;
using NejCommon.Controllers;
using NejCommon.Models;
namespace NejCommon.Utils;
public static class Extensions
{
public static IQueryable<TType> ApplyPagination<TType>(this IQueryable<TType> query, Pagination pag)
{
query = query.Skip(pag.Offset);
query = query.Take(pag.Count);
return query;
}
public static async Task<PaginationResponse<TResponseType>> ApplyPaginationRes<TType, TResponseType>(this IQueryable<TType> query, IServiceProvider providers, Pagination pag) where TResponseType : IAutomappedAttribute<TType, TResponseType>, new()
{
var totalCount = await query.CountAsync();
query = query.Skip(pag.Offset);
query = query.Take(pag.Count);
var projector = new TResponseType().GetProjectorFrom(providers);
return new PaginationResponse<TResponseType>
{
TotalCount = totalCount,
Offset = pag.Offset,
Count = pag.Count,
Data = query.Select(projector).AsAsyncEnumerable()
};
}
public static async Task<PaginationResponse<TResponseType>> ApplySearchPaginationRes<TType, TResponseType, TEntityBinder>(this IQueryable<TType> query, string? search, IServiceProvider providers, Pagination pag, List<Expression<Func<TType, string, bool>>> matchers)
where TType : class
where TResponseType : IAutomappedAttribute<TType, TResponseType>, new()
where TEntityBinder : EntityBinder<TType>
{
if (search != null)
{
var searchers = new List<Expression<Func<TType, bool>>>();
foreach (var matcher in matchers)
{
//reduce expression from TType, string, bool to TType, bool by passing the search string
var par = Expression.Parameter(typeof(TType));
var exp = Expression.Invoke(matcher, par, Expression.Constant(search));
var redExp = exp.Reduce();
var reducedMatcher = Expression.Lambda<Func<TType, bool>>(redExp, par);
searchers.Add(reducedMatcher);
}
var extPar = Expression.Parameter(typeof(TType));
var binder = (TEntityBinder)typeof(TEntityBinder).GetConstructor(new[] { typeof(AppDbContext) })!.Invoke(new object?[] { null });
var idMatcher = binder.RouteValues.First(x => x.Primary).Expression;
var reducedIdMatcher = Expression.Lambda<Func<TType, bool>>(Expression.Invoke(idMatcher, extPar, Expression.Constant(search)).Reduce(), extPar);
//Console.Writeline(reducedIdMatcher);
searchers.Add(reducedIdMatcher);
// Create an expression that ORs all the searchers
var agrPar = Expression.Parameter(typeof(TType));
var orExp = searchers.Aggregate((Expression)null, (current, searcher) =>
{
var body = Expression.Invoke(searcher, agrPar);
if (current == null)
{
return body;
}
return Expression.OrElse(current, body);
});
//reduce the epxression as much as possible
while (orExp.CanReduce)
{
orExp = orExp.Reduce();
}
var orLambda = Expression.Lambda<Func<TType, bool>>(orExp, agrPar);
//Console.Writeline(orLambda);
query = query.Where(orLambda);
}
return await query.ApplyPaginationRes<TType, TResponseType>(providers, pag);
}
[Projectable]
public static IQueryable<TType> ApplyDateOnlyFrameQ<TType>(this IQueryable<TType> query, Func<TType, DateOnly> dateAcessor, DateOnlyFrame frame) => query.Where(e => dateAcessor(e) >= frame.fromDate && dateAcessor(e) <= frame.toDate);
[Projectable]
public static IEnumerable<TType> ApplyDateOnlyFrameE<TType>(this ICollection<TType> query, Func<TType, DateOnly> dateAcessor, DateOnlyFrame frame) => query.Where(e => dateAcessor(e) >= frame.fromDate && dateAcessor(e) <= frame.toDate);
[Projectable]
public static IQueryable<TType> ApplyDateTimeFrameQ<TType>(this IQueryable<TType> query, string datePropery, DateTimeFrame frame) => query.Where(e => EF.Property<DateTime>(e, datePropery) >= frame.FromDate && EF.Property<DateTime>(e, datePropery) <= frame.ToDate);
[Projectable]
public static IEnumerable<TType> ApplyDateTimeFrameE<TType>(this ICollection<TType> query, Func<TType, DateTime> dateAcessor, DateTimeFrame frame) => query.Where(e => dateAcessor(e) >= frame.FromDate && dateAcessor(e) <= frame.ToDate);
[Projectable]
public static DateOnly ToDateOnly(this DateTime date) => new DateOnly(date.Year, date.Month, date.Day);
[Projectable]
public static DateTime ToDateTime(this DateOnly date) => new DateTime(date.Year, date.Month, date.Day);
[Projectable]
public static decimal Round(this decimal value, int decimals = 2) => Math.Round(value, decimals);
[Projectable]
public static decimal Floor(this decimal value, int decimals = 2) => Math.Floor(value * (decimal)Math.Pow(10, decimals)) / (decimal)Math.Pow(10, decimals);
[Projectable]
public static int RountToInt(this decimal value) => (int)Math.Round(value);
[Projectable]
public static int GetInt(this string value, NumberStyles? style = null, CultureInfo? culture = null) => int.Parse(value, style ?? NumberStyles.Number, culture ?? CultureInfo.InvariantCulture);
[Projectable]
public static decimal GetDecimal(this string value, NumberStyles? style = null, CultureInfo? culture = null) => decimal.Parse(value, style ?? NumberStyles.Number, culture ?? CultureInfo.InvariantCulture);
[Projectable]
public static float GetFloat(this string value, NumberStyles? style = null, CultureInfo? culture = null) => float.Parse(value, style ?? NumberStyles.Number, culture ?? CultureInfo.InvariantCulture);
}