Initial commit
This commit is contained in:
commit
858c1bebd2
95 changed files with 174562 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
.idea
|
||||
*.DotSettings.user
|
248
Alveus.ruleset
Normal file
248
Alveus.ruleset
Normal file
|
@ -0,0 +1,248 @@
|
|||
<?xml version="1.0"?>
|
||||
<RuleSet Name="Alveus StyleCop Rules" Description="Rules with IsEnabledByDefault = false are disabled."
|
||||
ToolsVersion="14.0">
|
||||
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.SpecialRules">
|
||||
<Rule Id="SA0001" Action="Warning"/> <!-- XML comment analysis disabled -->
|
||||
<Rule Id="SA0002" Action="Warning"/> <!-- Invalid settings file -->
|
||||
</Rules>
|
||||
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.SpacingRules">
|
||||
<Rule Id="SA1000" Action="Warning"/> <!-- Keywords should be spaced correctly -->
|
||||
<Rule Id="SA1001" Action="Warning"/> <!-- Commas should be spaced correctly -->
|
||||
<Rule Id="SA1002" Action="Warning"/> <!-- Semicolons should be spaced correctly -->
|
||||
<Rule Id="SA1003" Action="Warning"/> <!-- Symbols should be spaced correctly -->
|
||||
<Rule Id="SA1004" Action="Warning"/> <!-- Documentation lines should begin with single space -->
|
||||
<Rule Id="SA1005" Action="Warning"/> <!-- Single line comments should begin with single space -->
|
||||
<Rule Id="SA1006" Action="Warning"/> <!-- Preprocessor keywords should not be preceded by space -->
|
||||
<Rule Id="SA1007" Action="Warning"/> <!-- Operator keyword should be followed by space -->
|
||||
<Rule Id="SA1008" Action="Warning"/> <!-- Opening parenthesis should be spaced correctly -->
|
||||
<Rule Id="SA1009" Action="Warning"/> <!-- Closing parenthesis should be spaced correctly -->
|
||||
<Rule Id="SA1010" Action="Warning"/> <!-- Opening square brackets should be spaced correctly -->
|
||||
<Rule Id="SA1011" Action="Warning"/> <!-- Closing square brackets should be spaced correctly -->
|
||||
<Rule Id="SA1012" Action="Warning"/> <!-- Opening braces should be spaced correctly -->
|
||||
<Rule Id="SA1013" Action="Warning"/> <!-- Closing braces should be spaced correctly -->
|
||||
<Rule Id="SA1014" Action="Warning"/> <!-- Opening generic brackets should be spaced correctly -->
|
||||
<Rule Id="SA1015" Action="Warning"/> <!-- Closing generic brackets should be spaced correctly -->
|
||||
<Rule Id="SA1016" Action="Warning"/> <!-- Opening attribute brackets should be spaced correctly -->
|
||||
<Rule Id="SA1017" Action="Warning"/> <!-- Closing attribute brackets should be spaced correctly -->
|
||||
<Rule Id="SA1018" Action="Warning"/> <!-- Nullable type symbols should be spaced correctly -->
|
||||
<Rule Id="SA1019" Action="Warning"/> <!-- Member access symbols should be spaced correctly -->
|
||||
<Rule Id="SA1020" Action="Warning"/> <!-- Increment decrement symbols should be spaced correctly -->
|
||||
<Rule Id="SA1021" Action="Warning"/> <!-- Negative signs should be spaced correctly -->
|
||||
<Rule Id="SA1022" Action="Warning"/> <!-- Positive signs should be spaced correctly -->
|
||||
<Rule Id="SA1023"
|
||||
Action="Warning"/> <!-- Dereference and access of symbols should be spaced correctly -->
|
||||
<Rule Id="SA1024" Action="Warning"/> <!-- Colons should be spaced correctly -->
|
||||
<Rule Id="SA1025" Action="Warning"/> <!-- Code should not contain multiple whitespace in a row -->
|
||||
<Rule Id="SA1026"
|
||||
Action="Warning"/> <!-- Code should not contain space after new or stackalloc keyword in implicitly typed array allocation -->
|
||||
<Rule Id="SA1027" Action="Warning"/> <!-- Use tabs correctly -->
|
||||
<Rule Id="SA1028" Action="Warning"/> <!-- Code should not contain trailing whitespace -->
|
||||
</Rules>
|
||||
|
||||
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.ReadabilityRules">
|
||||
<Rule Id="SA1100"
|
||||
Action="Warning"/> <!-- Do not prefix calls with base unless local implementation exists -->
|
||||
<Rule Id="SA1101" Action="None"/> <!-- Prefix local calls with this -->
|
||||
<Rule Id="SA1102" Action="Warning"/> <!-- Query clause should follow previous clause -->
|
||||
<Rule Id="SA1103"
|
||||
Action="Warning"/> <!-- Query clauses should be on separate lines or all on one line -->
|
||||
<Rule Id="SA1104"
|
||||
Action="Warning"/> <!-- Query clause should begin on new line when previous clause spans multiple lines -->
|
||||
<Rule Id="SA1105"
|
||||
Action="Warning"/> <!-- Query clauses spanning multiple lines should begin on own line -->
|
||||
<Rule Id="SA1106" Action="Warning"/> <!-- Code should not contain empty statements -->
|
||||
<Rule Id="SA1107" Action="Warning"/> <!-- Code should not contain multiple statements on one line -->
|
||||
<Rule Id="SA1108" Action="Warning"/> <!-- Block statements should not contain embedded comments -->
|
||||
<Rule Id="SA1109" Action="None"/> <!-- Block statements should not contain embedded regions -->
|
||||
<Rule Id="SA1110"
|
||||
Action="Warning"/> <!-- Opening parenthesis or bracket should be on declaration line -->
|
||||
<Rule Id="SA1111" Action="Warning"/> <!-- Closing parenthesis should be on line of last parameter -->
|
||||
<Rule Id="SA1112"
|
||||
Action="Warning"/> <!-- Closing parenthesis should be on line of opening parenthesis -->
|
||||
<Rule Id="SA1113" Action="Warning"/> <!-- Comma should be on the same line as previous parameter -->
|
||||
<Rule Id="SA1114" Action="Warning"/> <!-- Parameter list should follow declaration -->
|
||||
<Rule Id="SA1115" Action="Warning"/> <!-- Parameter should follow comma -->
|
||||
<Rule Id="SA1116" Action="Warning"/> <!-- Split parameters should start on line after declaration -->
|
||||
<Rule Id="SA1117" Action="Warning"/> <!-- Parameters should be on same line or separate lines -->
|
||||
<Rule Id="SA1118" Action="Warning"/> <!-- Parameter should not span multiple lines -->
|
||||
<Rule Id="SA1120" Action="Warning"/> <!-- Comments should contain text -->
|
||||
<Rule Id="SA1121" Action="Warning"/> <!-- Use built-in type alias -->
|
||||
<Rule Id="SA1122" Action="Warning"/> <!-- Use string.Empty for empty strings -->
|
||||
<Rule Id="SA1123" Action="Warning"/> <!-- Do not place regions within elements -->
|
||||
<Rule Id="SA1124" Action="Warning"/> <!-- Do not use regions -->
|
||||
<Rule Id="SA1125" Action="Warning"/> <!-- Use shorthand for nullable types -->
|
||||
<Rule Id="SA1126" Action="None"/> <!-- Prefix calls correctly -->
|
||||
<Rule Id="SA1127" Action="Warning"/> <!-- Generic type constraints should be on their own line -->
|
||||
<Rule Id="SA1128" Action="Warning"/> <!-- Put constructor initializers on their own line -->
|
||||
<Rule Id="SA1129" Action="Warning"/> <!-- Do not use default value type constructor -->
|
||||
<Rule Id="SA1130" Action="Warning"/> <!-- Use lambda syntax -->
|
||||
<Rule Id="SA1131" Action="Warning"/> <!-- Use readable conditions -->
|
||||
<Rule Id="SA1132" Action="Warning"/> <!-- Do not combine fields -->
|
||||
<Rule Id="SA1133" Action="Warning"/> <!-- Do not combine attributes -->
|
||||
<Rule Id="SA1134" Action="Warning"/> <!-- Attributes should not share line -->
|
||||
<Rule Id="SA1135" Action="Warning"/> <!-- Using directives should be qualified -->
|
||||
<Rule Id="SA1136" Action="Warning"/> <!-- Enum values should be on separate lines -->
|
||||
<Rule Id="SA1137" Action="Warning"/> <!-- Elements should have the same indentation -->
|
||||
<Rule Id="SA1139" Action="Warning"/> <!-- Use literal suffix notation instead of casting -->
|
||||
<Rule Id="SX1101" Action="Warning"/> <!-- Do not prefix local calls with 'this.' -->
|
||||
</Rules>
|
||||
|
||||
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.OrderingRules">
|
||||
<Rule Id="SA1200" Action="None"/> <!-- Using directives should be placed correctly -->
|
||||
<Rule Id="SA1201" Action="Warning"/> <!-- Elements should appear in the correct order -->
|
||||
<Rule Id="SA1202" Action="Warning"/> <!-- Elements should be ordered by access -->
|
||||
<Rule Id="SA1203" Action="Warning"/> <!-- Constants should appear before fields -->
|
||||
<Rule Id="SA1204" Action="Warning"/> <!-- Static elements should appear before instance elements -->
|
||||
<Rule Id="SA1205" Action="Warning"/> <!-- Partial elements should declare access -->
|
||||
<Rule Id="SA1206" Action="Warning"/> <!-- Declaration keywords should follow order -->
|
||||
<Rule Id="SA1207" Action="Warning"/> <!-- Protected should come before internal -->
|
||||
<Rule Id="SA1208"
|
||||
Action="Warning"/> <!-- System using directives should be placed before other using directives -->
|
||||
<Rule Id="SA1209"
|
||||
Action="Warning"/> <!-- Using alias directives should be placed after other using directives -->
|
||||
<Rule Id="SA1210"
|
||||
Action="Warning"/> <!-- Using directives should be ordered alphabetically by namespace -->
|
||||
<Rule Id="SA1211"
|
||||
Action="Warning"/> <!-- Using alias directives should be ordered alphabetically by alias name -->
|
||||
<Rule Id="SA1212" Action="Warning"/> <!-- Property accessors should follow order -->
|
||||
<Rule Id="SA1213" Action="Warning"/> <!-- Event accessors should follow order -->
|
||||
<Rule Id="SA1214" Action="Warning"/> <!-- Readonly fields should appear before non-readonly fields -->
|
||||
<Rule Id="SA1216"
|
||||
Action="Warning"/> <!-- Using static directives should be placed at the correct location -->
|
||||
<Rule Id="SA1217" Action="Warning"/> <!-- Using static directives should be ordered alphabetically -->
|
||||
</Rules>
|
||||
|
||||
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.NamingRules">
|
||||
<Rule Id="SA1300" Action="Warning"/> <!-- Element should begin with upper-case letter -->
|
||||
<Rule Id="SA1301" Action="None"/> <!-- Element should begin with lower-case letter -->
|
||||
<Rule Id="SA1302" Action="Warning"/> <!-- Interface names should begin with I -->
|
||||
<Rule Id="SA1303" Action="Warning"/> <!-- Const field names should begin with upper-case letter -->
|
||||
<Rule Id="SA1304"
|
||||
Action="Warning"/> <!-- Non-private readonly fields should begin with upper-case letter -->
|
||||
<Rule Id="SA1305" Action="None"/> <!-- Field names should not use Hungarian notation -->
|
||||
<Rule Id="SA1306" Action="Warning"/> <!-- Field names should begin with lower-case letter -->
|
||||
<Rule Id="SA1307" Action="Warning"/> <!-- Accessible fields should begin with upper-case letter -->
|
||||
<Rule Id="SA1308" Action="Warning"/> <!-- Variable names should not be prefixed -->
|
||||
<Rule Id="SA1309" Action="None"/> <!-- Field names should not begin with underscore -->
|
||||
<Rule Id="SA1310" Action="Warning"/> <!-- Field names should not contain underscore -->
|
||||
<Rule Id="SA1311"
|
||||
Action="Warning"/> <!-- Static readonly fields should begin with upper-case letter -->
|
||||
<Rule Id="SA1312" Action="Warning"/> <!-- Variable names should begin with lower-case letter -->
|
||||
<Rule Id="SA1313" Action="Warning"/> <!-- Parameter names should begin with lower-case letter -->
|
||||
<Rule Id="SA1314" Action="Warning"/> <!-- Type parameter names should begin with T -->
|
||||
<Rule Id="SX1309" Action="None"/> <!-- Field names should begin with underscore -->
|
||||
<Rule Id="SX1309S" Action="None"/> <!-- Static field names should begin with underscore -->
|
||||
</Rules>
|
||||
|
||||
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.MaintainabilityRules">
|
||||
<Rule Id="SA1119" Action="Warning"/> <!-- Statement should not use unnecessary parenthesis -->
|
||||
<Rule Id="SA1400" Action="Warning"/> <!-- Access modifier should be declared -->
|
||||
<Rule Id="SA1401" Action="Warning"/> <!-- Fields should be private -->
|
||||
<Rule Id="SA1402" Action="Warning"/> <!-- File may only contain a single type -->
|
||||
<Rule Id="SA1403" Action="Warning"/> <!-- File may only contain a single namespace -->
|
||||
<Rule Id="SA1404" Action="Warning"/> <!-- Code analysis suppression should have justification -->
|
||||
<Rule Id="SA1405" Action="Warning"/> <!-- Debug.Assert should provide message text -->
|
||||
<Rule Id="SA1406" Action="Warning"/> <!-- Debug.Fail should provide message text -->
|
||||
<Rule Id="SA1407" Action="Warning"/> <!-- Arithmetic expressions should declare precedence -->
|
||||
<Rule Id="SA1408" Action="Warning"/> <!-- Conditional expressions should declare precedence -->
|
||||
<Rule Id="SA1409" Action="None"/> <!-- Remove unnecessary code -->
|
||||
<Rule Id="SA1410" Action="Warning"/> <!-- Remove delegate parenthesis when possible -->
|
||||
<Rule Id="SA1411"
|
||||
Action="Warning"/> <!-- Attribute constructor should not use unnecessary parenthesis -->
|
||||
<Rule Id="SA1412" Action="None"/> <!-- Store files as UTF-8 with byte order mark -->
|
||||
<Rule Id="SA1413" Action="None"/> <!-- Use trailing comma in multi-line initializers -->
|
||||
</Rules>
|
||||
|
||||
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.LayoutRules">
|
||||
<Rule Id="SA1500" Action="Warning"/> <!-- Braces for multi-line statements should not share line -->
|
||||
<Rule Id="SA1501" Action="Warning"/> <!-- Statement should not be on a single line -->
|
||||
<Rule Id="SA1502" Action="Warning"/> <!-- Element should not be on a single line -->
|
||||
<Rule Id="SA1503" Action="Warning"/> <!-- Braces should not be omitted -->
|
||||
<Rule Id="SA1504" Action="Warning"/> <!-- All accessors should be single-line or multi-line -->
|
||||
<Rule Id="SA1505" Action="Warning"/> <!-- Opening braces should not be followed by blank line -->
|
||||
<Rule Id="SA1506"
|
||||
Action="Warning"/> <!-- Element documentation headers should not be followed by blank line -->
|
||||
<Rule Id="SA1507" Action="Warning"/> <!-- Code should not contain multiple blank lines in a row -->
|
||||
<Rule Id="SA1508" Action="Warning"/> <!-- Closing braces should not be preceded by blank line -->
|
||||
<Rule Id="SA1509" Action="Warning"/> <!-- Opening braces should not be preceded by blank line -->
|
||||
<Rule Id="SA1510"
|
||||
Action="Warning"/> <!-- Chained statement blocks should not be preceded by blank line -->
|
||||
<Rule Id="SA1511" Action="Warning"/> <!-- While-do footer should not be preceded by blank line -->
|
||||
<Rule Id="SA1512" Action="Warning"/> <!-- Single-line comments should not be followed by blank line -->
|
||||
<Rule Id="SA1513" Action="Warning"/> <!-- Closing brace should be followed by blank line -->
|
||||
<Rule Id="SA1514"
|
||||
Action="Warning"/> <!-- Element documentation header should be preceded by blank line -->
|
||||
<Rule Id="SA1515" Action="Warning"/> <!-- Single-line comment should be preceded by blank line -->
|
||||
<Rule Id="SA1516" Action="None"/> <!-- Elements should be separated by blank line -->
|
||||
<Rule Id="SA1517" Action="Warning"/> <!-- Code should not contain blank lines at start of file -->
|
||||
<Rule Id="SA1518" Action="Warning"/> <!-- Use line endings correctly at end of file -->
|
||||
<Rule Id="SA1519"
|
||||
Action="Warning"/> <!-- Braces should not be omitted from multi-line child statement -->
|
||||
<Rule Id="SA1520" Action="Warning"/> <!-- Use braces consistently -->
|
||||
</Rules>
|
||||
|
||||
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.DocumentationRules">
|
||||
<Rule Id="SA1600" Action="Warning"/> <!-- Elements should be documented -->
|
||||
<Rule Id="SA1601" Action="Warning"/> <!-- Partial elements should be documented -->
|
||||
<Rule Id="SA1602" Action="Warning"/> <!-- Enumeration items should be documented -->
|
||||
<Rule Id="SA1603" Action="None"/> <!-- Documentation should contain valid XML -->
|
||||
<Rule Id="SA1604" Action="Warning"/> <!-- Element documentation should have summary -->
|
||||
<Rule Id="SA1605" Action="Warning"/> <!-- Partial element documentation should have summary -->
|
||||
<Rule Id="SA1606" Action="Warning"/> <!-- Element documentation should have summary text -->
|
||||
<Rule Id="SA1607" Action="Warning"/> <!-- Partial element documentation should have summary text -->
|
||||
<Rule Id="SA1608" Action="Warning"/> <!-- Element documentation should not have default summary -->
|
||||
<Rule Id="SA1609" Action="None"/> <!-- Property documentation should have value -->
|
||||
<Rule Id="SA1610" Action="Warning"/> <!-- Property documentation should have value text -->
|
||||
<Rule Id="SA1611" Action="Warning"/> <!-- Element parameters should be documented -->
|
||||
<Rule Id="SA1612"
|
||||
Action="Warning"/> <!-- Element parameter documentation should match element parameters -->
|
||||
<Rule Id="SA1613"
|
||||
Action="Warning"/> <!-- Element parameter documentation should declare parameter name -->
|
||||
<Rule Id="SA1614" Action="Warning"/> <!-- Element parameter documentation should have text -->
|
||||
<Rule Id="SA1615" Action="None"/> <!-- Element return value should be documented -->
|
||||
<Rule Id="SA1616" Action="Warning"/> <!-- Element return value documentation should have text -->
|
||||
<Rule Id="SA1617" Action="Warning"/> <!-- Void return value should not be documented -->
|
||||
<Rule Id="SA1618" Action="Warning"/> <!-- Generic type parameters should be documented -->
|
||||
<Rule Id="SA1619"
|
||||
Action="Warning"/> <!-- Generic type parameters should be documented partial class -->
|
||||
<Rule Id="SA1620"
|
||||
Action="Warning"/> <!-- Generic type parameter documentation should match type parameters -->
|
||||
<Rule Id="SA1621"
|
||||
Action="Warning"/> <!-- Generic type parameter documentation should declare parameter name -->
|
||||
<Rule Id="SA1622" Action="Warning"/> <!-- Generic type parameter documentation should have text -->
|
||||
<Rule Id="SA1623" Action="None"/> <!-- Property summary documentation should match accessors -->
|
||||
<Rule Id="SA1624"
|
||||
Action="Warning"/> <!-- Property summary documentation should omit accessor with restricted access -->
|
||||
<Rule Id="SA1625" Action="Warning"/> <!-- Element documentation should not be copied and pasted -->
|
||||
<Rule Id="SA1626"
|
||||
Action="Warning"/> <!-- Single-line comments should not use documentation style slashes -->
|
||||
<Rule Id="SA1627" Action="Warning"/> <!-- Documentation text should not be empty -->
|
||||
<Rule Id="SA1628" Action="None"/> <!-- Documentation text should begin with a capital letter -->
|
||||
<Rule Id="SA1629" Action="Warning"/> <!-- Documentation text should end with a period -->
|
||||
<Rule Id="SA1630" Action="None"/> <!-- Documentation text should contain whitespace -->
|
||||
<Rule Id="SA1631" Action="None"/> <!-- Documentation should meet character percentage -->
|
||||
<Rule Id="SA1632" Action="None"/> <!-- Documentation text should meet minimum character length -->
|
||||
<Rule Id="SA1633" Action="Warning"/> <!-- File should have header -->
|
||||
<Rule Id="SA1634" Action="Warning"/> <!-- File header should show copyright -->
|
||||
<Rule Id="SA1635" Action="Warning"/> <!-- File header should have copyright text -->
|
||||
<Rule Id="SA1636" Action="None"/> <!-- File header copyright text should match -->
|
||||
<Rule Id="SA1637" Action="Warning"/> <!-- File header should contain file name -->
|
||||
<Rule Id="SA1638"
|
||||
Action="Warning"/> <!-- File header file name documentation should match file name -->
|
||||
<Rule Id="SA1639" Action="None"/> <!-- File header should have summary -->
|
||||
<Rule Id="SA1640" Action="Warning"/> <!-- File header should have valid company text -->
|
||||
<Rule Id="SA1641" Action="Warning"/> <!-- File header company name text should match -->
|
||||
<Rule Id="SA1642"
|
||||
Action="Warning"/> <!-- Constructor summary documentation should begin with standard text -->
|
||||
<Rule Id="SA1643"
|
||||
Action="Warning"/> <!-- Destructor summary documentation should begin with standard text -->
|
||||
<Rule Id="SA1644" Action="None"/> <!-- Documentation headers should not contain blank lines -->
|
||||
<Rule Id="SA1645" Action="None"/> <!-- Included documentation file does not exist -->
|
||||
<Rule Id="SA1646" Action="None"/> <!-- Included documentation XPath does not exist -->
|
||||
<Rule Id="SA1647" Action="None"/> <!-- Include node does not contain valid file and path -->
|
||||
<Rule Id="SA1648" Action="Warning"/> <!-- Inheritdoc should be used with inheriting class -->
|
||||
<Rule Id="SA1649" Action="Warning"/> <!-- File name should match first type name -->
|
||||
<Rule Id="SA1650" Action="None"/> <!-- Element documentation should be spelled correctly -->
|
||||
<Rule Id="SA1651" Action="Warning"/> <!-- Do not use placeholder elements -->
|
||||
</Rules>
|
||||
</RuleSet>
|
31
Astral.ApiServer/Astral.ApiServer.csproj
Normal file
31
Astral.ApiServer/Astral.ApiServer.csproj
Normal file
|
@ -0,0 +1,31 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<CodeAnalysisRuleSet>../Alveus.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0"/>
|
||||
<PackageReference Include="MyCSharp.HttpUserAgentParser.AspNetCore" Version="3.0.9"/>
|
||||
<PackageReference Include="MyCSharp.HttpUserAgentParser.MemoryCache" Version="3.0.9"/>
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0-dev-02301"/>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="7.1.0"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.1.0"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="7.1.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Controllers\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Astral.Services\Astral.Services.csproj"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
31
Astral.ApiServer/Core/ApiErrorCodes.cs
Normal file
31
Astral.ApiServer/Core/ApiErrorCodes.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
// <copyright file="ApiErrorCodes.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.ApiServer.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Api specific error codes.
|
||||
/// </summary>
|
||||
public static class ApiErrorCodes
|
||||
{
|
||||
/// <summary>
|
||||
/// Validation failed.
|
||||
/// </summary>
|
||||
public const string UnknownError = "unknown-error";
|
||||
|
||||
/// <summary>
|
||||
/// Illegal method call.
|
||||
/// </summary>
|
||||
public const string UnknownMethod = "unknown-method";
|
||||
|
||||
/// <summary>
|
||||
/// Illegal method call.
|
||||
/// </summary>
|
||||
public const string IllegalMethod = "illegal-method";
|
||||
|
||||
/// <summary>
|
||||
/// Validation failed.
|
||||
/// </summary>
|
||||
public const string UnsupportedBody = "unsupported-body";
|
||||
}
|
36
Astral.ApiServer/Extensions/JwtOptionsExtensions.cs
Normal file
36
Astral.ApiServer/Extensions/JwtOptionsExtensions.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
// <copyright file="JwtOptionsExtensions.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Text;
|
||||
using Astral.Core.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Astral.ApiServer.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="JwtOptions" />.
|
||||
/// </summary>
|
||||
public static class JwtOptionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Generate an instance of <see cref="TokenValidationParameters" /> from <see cref="JwtOptions" />.
|
||||
/// </summary>
|
||||
/// <param name="config">Instance of <see cref="JwtOptions" />.</param>
|
||||
/// <returns>Instance of <see cref="TokenValidationParameters" />.</returns>
|
||||
public static TokenValidationParameters ToTokenValidationParameters(this JwtOptions config)
|
||||
{
|
||||
return new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = config.ValidateIssuer,
|
||||
ValidateAudience = config.ValidateAudience,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidAudience = config.Audience,
|
||||
ValidAudiences =
|
||||
[config.Audience],
|
||||
ValidIssuer = config.Issuer,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SecretKey))
|
||||
};
|
||||
}
|
||||
}
|
62
Astral.ApiServer/HostedService/StartupService.cs
Normal file
62
Astral.ApiServer/HostedService/StartupService.cs
Normal file
|
@ -0,0 +1,62 @@
|
|||
// <copyright file="StartupService.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Infrastructure;
|
||||
using Astral.Services.Interfaces;
|
||||
|
||||
namespace Astral.ApiServer.HostedService;
|
||||
|
||||
/// <summary>
|
||||
/// Processes to be executed at startup.
|
||||
/// </summary>
|
||||
public class StartupService : IHostedService
|
||||
{
|
||||
private readonly ILogger<StartupService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StartupService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="serviceScopeFactory">Instance of <see cref="IServiceScopeFactory" />.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger" />.</param>
|
||||
public StartupService(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<StartupService> logger)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Running startup processes");
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
|
||||
// Database migrations.
|
||||
var dbMigrator = scope.ServiceProvider.GetRequiredService<IDatabaseMigrator>();
|
||||
await dbMigrator.MigrateDatabaseAsync(AppDomain.CurrentDomain.BaseDirectory + "Migrations");
|
||||
|
||||
// DAL setup.
|
||||
var dalSetup = scope.ServiceProvider.GetService<IDataAccessLayerSetup>();
|
||||
if (dalSetup is not null)
|
||||
{
|
||||
await dalSetup.Setup();
|
||||
}
|
||||
|
||||
// Email address domain blacklist update.
|
||||
var emDomainBlacklist = scope.ServiceProvider.GetRequiredService<IEmailDomainBlacklistService>();
|
||||
await emDomainBlacklist.UpdateBlacklist();
|
||||
|
||||
// Initial user setup.
|
||||
var initialUserSetup = scope.ServiceProvider.GetRequiredService<IInitialUserService>();
|
||||
await initialUserSetup.CreateFirstUserIfRequiredAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
110
Astral.ApiServer/Middleware/ExceptionMiddleware.cs
Normal file
110
Astral.ApiServer/Middleware/ExceptionMiddleware.cs
Normal file
|
@ -0,0 +1,110 @@
|
|||
// <copyright file="ExceptionMiddleware.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Net;
|
||||
using Astral.ApiServer.Core;
|
||||
using Astral.ApiServer.Models.Common;
|
||||
using Astral.Core.Constants;
|
||||
using Astral.Core.Exceptions;
|
||||
using FluentValidation;
|
||||
|
||||
namespace Astral.ApiServer.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Handle exceptions caught in a request.
|
||||
/// </summary>
|
||||
public class ExceptionMiddleware
|
||||
{
|
||||
private readonly ILogger<ExceptionMiddleware> _logger;
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExceptionMiddleware" /> class.
|
||||
/// </summary>
|
||||
/// <param name="next">Instance of <see cref="RequestDelegate" />.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger" />.</param>
|
||||
public ExceptionMiddleware(
|
||||
RequestDelegate next,
|
||||
ILogger<ExceptionMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke middleware.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">Instance of <see cref="HttpContext" />.</param>
|
||||
public async Task InvokeAsync(HttpContext httpContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(httpContext);
|
||||
}
|
||||
catch (ServiceException exception)
|
||||
{
|
||||
await HandleServiceExceptionAsync(httpContext, exception);
|
||||
}
|
||||
catch (ValidationException exception)
|
||||
{
|
||||
await HandleValidationExceptionAsync(httpContext, exception);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Unhandled Exception: {path} using {method}: {exception}",
|
||||
httpContext.Request.Path,
|
||||
httpContext.Request.Method,
|
||||
exception);
|
||||
await HandleOtherExceptionAsync(httpContext);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Api exception.
|
||||
/// </summary>
|
||||
/// <param name="context">Instance of <see cref="HttpContext" />.</param>
|
||||
/// <param name="exception">Instance of <see cref="ValidationException" />.</param>
|
||||
private static async Task HandleServiceExceptionAsync(HttpContext context, ServiceException exception)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.StatusCode = (int)exception.HttpStatusCode;
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResultModel
|
||||
{
|
||||
Error = exception.ErrorCode,
|
||||
Message = exception.ErrorMessage
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validation failed.
|
||||
/// </summary>
|
||||
/// <param name="context">Instance of <see cref="HttpContext" />.</param>
|
||||
/// <param name="exception">Instance of <see cref="ValidationException" />.</param>
|
||||
private static async Task HandleValidationExceptionAsync(HttpContext context, ValidationException exception)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.StatusCode = StatusCodes.Status422UnprocessableEntity;
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResultModel
|
||||
{
|
||||
Error = CoreErrorCodes.ValidationError,
|
||||
Message = exception.Message
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unexpected exception.
|
||||
/// </summary>
|
||||
/// <param name="context">Instance of <see cref="HttpContext" />.</param>
|
||||
private static async Task HandleOtherExceptionAsync(HttpContext context)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResultModel
|
||||
{
|
||||
Error = ApiErrorCodes.UnknownError,
|
||||
Message = "Something went wrong. Try again later"
|
||||
});
|
||||
}
|
||||
}
|
93
Astral.ApiServer/Middleware/StatusCodeMiddleware.cs
Normal file
93
Astral.ApiServer/Middleware/StatusCodeMiddleware.cs
Normal file
|
@ -0,0 +1,93 @@
|
|||
// <copyright file="StatusCodeMiddleware.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.ApiServer.Core;
|
||||
using Astral.ApiServer.Models.Common;
|
||||
using Astral.Core.Constants;
|
||||
|
||||
namespace Astral.ApiServer.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Handle status code responses.
|
||||
/// </summary>
|
||||
public class StatusCodeMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StatusCodeMiddleware" /> class.
|
||||
/// </summary>
|
||||
/// <param name="next">Instance of <see cref="RequestDelegate" />.</param>
|
||||
public StatusCodeMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke middleware.
|
||||
/// </summary>
|
||||
/// <param name="context">Instance of <see cref="HttpContext" />.</param>
|
||||
public Task Invoke(HttpContext context)
|
||||
{
|
||||
return InvokeAsync(context);
|
||||
}
|
||||
|
||||
private async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
await _next(context);
|
||||
|
||||
switch (context.Response.StatusCode)
|
||||
{
|
||||
case 401:
|
||||
context.Response.Headers.Clear();
|
||||
context.Response.ContentType = "text/json";
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResultModel
|
||||
{
|
||||
Error = CoreErrorCodes.Unauthorized,
|
||||
Message = "You're not authorized to do that"
|
||||
});
|
||||
break;
|
||||
|
||||
case 404:
|
||||
context.Response.Headers.Clear();
|
||||
context.Response.ContentType = "text/json";
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResultModel
|
||||
{
|
||||
Error = ApiErrorCodes.UnknownMethod,
|
||||
Message = "Unknown method"
|
||||
});
|
||||
break;
|
||||
|
||||
case 405:
|
||||
context.Response.Headers.Clear();
|
||||
context.Response.ContentType = "text/json";
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResultModel
|
||||
{
|
||||
Error = ApiErrorCodes.IllegalMethod,
|
||||
Message = "Illegal method"
|
||||
});
|
||||
break;
|
||||
|
||||
case 415:
|
||||
context.Response.Headers.Clear();
|
||||
context.Response.ContentType = "text/json";
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResultModel
|
||||
{
|
||||
Error = ApiErrorCodes.UnsupportedBody,
|
||||
Message = "Unsupported body/media type"
|
||||
});
|
||||
break;
|
||||
|
||||
case 500:
|
||||
context.Response.Headers.Clear();
|
||||
context.Response.ContentType = "text/json";
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResultModel
|
||||
{
|
||||
Error = ApiErrorCodes.UnknownError,
|
||||
Message = "Unknown error"
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
31
Astral.ApiServer/Models/Common/ErrorResultModel.cs
Normal file
31
Astral.ApiServer/Models/Common/ErrorResultModel.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
// <copyright file="ErrorResultModel.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Astral.ApiServer.Models.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Error result model.
|
||||
/// </summary>
|
||||
public class ErrorResultModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Status: failure.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public const string Status = "failure";
|
||||
|
||||
/// <summary>
|
||||
/// Error code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("error")]
|
||||
public string Error { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("message")]
|
||||
public string Message { get; set; }
|
||||
}
|
19
Astral.ApiServer/Models/Common/ResultModel.cs
Normal file
19
Astral.ApiServer/Models/Common/ResultModel.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
// <copyright file="ResultModel.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Astral.ApiServer.Models.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Generic result model.
|
||||
/// </summary>
|
||||
public class ResultModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Either "success" or "failure".
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; }
|
||||
}
|
108
Astral.ApiServer/Program.cs
Normal file
108
Astral.ApiServer/Program.cs
Normal file
|
@ -0,0 +1,108 @@
|
|||
// <copyright file="Program.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.ApiServer.Extensions;
|
||||
using Astral.ApiServer.HostedService;
|
||||
using Astral.ApiServer.Middleware;
|
||||
using Astral.Core.Options;
|
||||
using Astral.Services.Options;
|
||||
using Astral.Services.Profiles;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using MyCSharp.HttpUserAgentParser.AspNetCore.DependencyInjection;
|
||||
using MyCSharp.HttpUserAgentParser.MemoryCache.DependencyInjection;
|
||||
using Serilog;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Configuration
|
||||
.AddJsonFile("appsettings.json", true, true)
|
||||
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true)
|
||||
.AddEnvironmentVariables();
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console()
|
||||
.CreateLogger();
|
||||
|
||||
builder.Services.AddSerilog();
|
||||
|
||||
builder.Services.AddHttpUserAgentMemoryCachedParser(opt =>
|
||||
{
|
||||
opt.CacheEntryOptions.SlidingExpiration = TimeSpan.FromMinutes(30);
|
||||
opt.CacheOptions.SizeLimit = 1024;
|
||||
}).AddHttpUserAgentParserAccessor();
|
||||
|
||||
builder.Services.AddHostedService<StartupService>();
|
||||
|
||||
builder.Services.AddAstralCore();
|
||||
builder.Services.AddAstralServices();
|
||||
builder.Services.AddAstralDAL();
|
||||
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddControllers();
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(setup =>
|
||||
{
|
||||
var jwtSecurityScheme = new OpenApiSecurityScheme
|
||||
{
|
||||
BearerFormat = "JWT",
|
||||
Name = "JWT Authentication",
|
||||
In = ParameterLocation.Header,
|
||||
Type = SecuritySchemeType.Http,
|
||||
Scheme = JwtBearerDefaults.AuthenticationScheme,
|
||||
Description = "Paste your access token below",
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Id = JwtBearerDefaults.AuthenticationScheme,
|
||||
Type = ReferenceType.SecurityScheme
|
||||
}
|
||||
};
|
||||
|
||||
setup.AddSecurityDefinition(jwtSecurityScheme.Reference.Id, jwtSecurityScheme);
|
||||
|
||||
setup.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{ jwtSecurityScheme, Array.Empty<string>() }
|
||||
});
|
||||
});
|
||||
|
||||
// Setup authentication.
|
||||
var jwtConfig = new JwtOptions();
|
||||
builder.Configuration.Bind("JWT", jwtConfig);
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options => options.TokenValidationParameters = jwtConfig.ToTokenValidationParameters());
|
||||
|
||||
builder.Services.AddAuthentication();
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// Setup automapping.
|
||||
builder.Services.AddAutoMapper(typeof(AutomapperProfile).Assembly);
|
||||
|
||||
// Setup configuration.
|
||||
builder.Services.Configure<DatabaseOptions>(builder.Configuration.GetSection("Database"));
|
||||
builder.Services.Configure<PwdHashOptions>(builder.Configuration.GetSection("PwdHash"));
|
||||
builder.Services.Configure<InitialUserOptions>(builder.Configuration.GetSection("InitialUser"));
|
||||
builder.Services.Configure<RegistrationOptions>(builder.Configuration.GetSection("Registration"));
|
||||
builder.Services.Configure<EmailDomainBlacklistOptions>(
|
||||
builder.Configuration.GetSection("EmailDomainBlacklist"));
|
||||
builder.Services.Configure<JwtOptions>(builder.Configuration.GetSection("JWT"));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Error handling middleware.
|
||||
app.UseMiddleware<ExceptionMiddleware>();
|
||||
app.UseMiddleware<StatusCodeMiddleware>();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
|
||||
await app.RunAsync();
|
8
Astral.ApiServer/appsettings.Development.json
Normal file
8
Astral.ApiServer/appsettings.Development.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
43
Astral.ApiServer/appsettings.json
Normal file
43
Astral.ApiServer/appsettings.json
Normal file
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Warning"
|
||||
}
|
||||
},
|
||||
"Database": {
|
||||
"ConnectionString": ""
|
||||
},
|
||||
"PwdHash": {
|
||||
"DegreeOfParallelism": 4,
|
||||
"NumberOfIterations": 3,
|
||||
"MemoryToUseKb": 16,
|
||||
"SaltSize": 64,
|
||||
"HashSize": 128
|
||||
},
|
||||
"InitialUser": {
|
||||
"Username": "admin",
|
||||
"Email": "admin@changeme.com",
|
||||
"Password": "Change!Me1234"
|
||||
},
|
||||
"EmailDomainBlacklist": {
|
||||
"Enabled": true,
|
||||
"MasterList": "https://raw.githubusercontent.com/disposable/disposable-email-domains/refs/heads/master/domains.txt"
|
||||
},
|
||||
"Registration": {
|
||||
"RequireEmailValidation": false,
|
||||
"DefaultHeroImageUrl": "heroimage",
|
||||
"DefaultThumbnailImageUrl": "thumbnail"
|
||||
},
|
||||
"JWT": {
|
||||
"SecretKey": "your-secret-key",
|
||||
"Issuer": "your-issuer",
|
||||
"Audience": "your-audience",
|
||||
"AccessTokenExpiration": "7 days",
|
||||
"RefreshTokenExpiration": "14 days",
|
||||
"ValidateIssuer": true,
|
||||
"ValidateAudience": true
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
24
Astral.Core/Astral.Core.csproj
Normal file
24
Astral.Core/Astral.Core.csproj
Normal file
|
@ -0,0 +1,24 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<CodeAnalysisRuleSet>../Alveus.ruleset</CodeAnalysisRuleSet>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="13.0.1"/>
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0"/>
|
||||
<PackageReference Include="Injectio" Version="4.0.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0"/>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="TimeSpanParserUtil" Version="1.2.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,26 @@
|
|||
// <copyright file="ColumnMappingAttribute.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Core.Attributes.EntityAnnotation;
|
||||
|
||||
/// <summary>
|
||||
/// Define the column name this property relates to.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ColumnMappingAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ColumnMappingAttribute" /> class.
|
||||
/// </summary>
|
||||
/// <param name="name">Database column name.</param>
|
||||
public ColumnMappingAttribute(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Database table column name.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// <copyright file="PrimaryKeyAttribute.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Core.Attributes.EntityAnnotation;
|
||||
|
||||
/// <summary>
|
||||
/// Treat this property as the primary key in the database.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class PrimaryKeyAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PrimaryKeyAttribute" /> class.
|
||||
/// </summary>
|
||||
/// <param name="autoGenerated">If the primary key auto-generated by the database.</param>
|
||||
public PrimaryKeyAttribute(bool autoGenerated = false)
|
||||
{
|
||||
AutoGenerated = autoGenerated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should this primary key be auto-generated by the database? (i.e. Auto incrementing).
|
||||
/// </summary>
|
||||
public bool AutoGenerated { get; }
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// <copyright file="TableMappingAttribute.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Core.Attributes.EntityAnnotation;
|
||||
|
||||
/// <summary>
|
||||
/// Define the database table name for this entity.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public class TableMappingAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TableMappingAttribute" /> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The database table name.</param>
|
||||
public TableMappingAttribute(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Database table name.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
}
|
36
Astral.Core/Constants/CoreErrorCodes.cs
Normal file
36
Astral.Core/Constants/CoreErrorCodes.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
// <copyright file="CoreErrorCodes.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Core.Constants;
|
||||
|
||||
/// <summary>
|
||||
/// Error codes to be provided in a response.
|
||||
/// </summary>
|
||||
public static class CoreErrorCodes
|
||||
{
|
||||
/// <summary>
|
||||
/// Unauthorised access.
|
||||
/// </summary>
|
||||
public const string Unauthorized = "unauthorized";
|
||||
|
||||
/// <summary>
|
||||
/// The request body is missing data.
|
||||
/// </summary>
|
||||
public const string NoDataProvided = "missing-data";
|
||||
|
||||
/// <summary>
|
||||
/// Validation failed.
|
||||
/// </summary>
|
||||
public const string ValidationError = "validation-fail";
|
||||
|
||||
/// <summary>
|
||||
/// Entity not found.
|
||||
/// </summary>
|
||||
public const string NotFoundError = "not-found";
|
||||
|
||||
/// <summary>
|
||||
/// An unexpected error.
|
||||
/// </summary>
|
||||
public const string UnexpectedError = "unexpected-error";
|
||||
}
|
30
Astral.Core/Constants/UserRole.cs
Normal file
30
Astral.Core/Constants/UserRole.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
// <copyright file="UserRole.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Core.Constants;
|
||||
|
||||
/// <summary>
|
||||
/// Available user roles.
|
||||
/// </summary>
|
||||
public static class UserRole
|
||||
{
|
||||
/// <summary>
|
||||
/// User role.
|
||||
/// </summary>
|
||||
public const string User = "user";
|
||||
|
||||
/// <summary>
|
||||
/// Administrator role.
|
||||
/// </summary>
|
||||
public const string Admin = "admin";
|
||||
|
||||
/// <summary>
|
||||
/// Available values.
|
||||
/// </summary>
|
||||
public static readonly string[] AvailableRoles =
|
||||
[
|
||||
User,
|
||||
Admin
|
||||
];
|
||||
}
|
27
Astral.Core/Entities/EmailDomainBlacklist.cs
Normal file
27
Astral.Core/Entities/EmailDomainBlacklist.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
// <copyright file="EmailDomainBlacklist.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Attributes.EntityAnnotation;
|
||||
|
||||
namespace Astral.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Entity representing a blacklisted email address domain.
|
||||
/// </summary>
|
||||
[TableMapping("emailDomainBlacklist")]
|
||||
public class EmailDomainBlacklist
|
||||
{
|
||||
/// <summary>
|
||||
/// The blacklisted domain.
|
||||
/// </summary>
|
||||
[PrimaryKey]
|
||||
[ColumnMapping("domain")]
|
||||
public string Domain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the record was created.
|
||||
/// </summary>
|
||||
[ColumnMapping("createdAt")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
87
Astral.Core/Entities/User.cs
Normal file
87
Astral.Core/Entities/User.cs
Normal file
|
@ -0,0 +1,87 @@
|
|||
// <copyright file="User.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Attributes.EntityAnnotation;
|
||||
|
||||
namespace Astral.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// User entity.
|
||||
/// </summary>
|
||||
[TableMapping("users")]
|
||||
public class User
|
||||
{
|
||||
/// <summary>
|
||||
/// User id.
|
||||
/// </summary>
|
||||
[ColumnMapping("id")]
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the user was created.
|
||||
/// </summary>
|
||||
[ColumnMapping("createdAt")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the user's entity was last updated.
|
||||
/// </summary>
|
||||
[ColumnMapping("updatedAt")]
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's name.
|
||||
/// </summary>
|
||||
[ColumnMapping("username")]
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's email address.
|
||||
/// </summary>
|
||||
[ColumnMapping("email")]
|
||||
public string EmailAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Authentication hash.
|
||||
/// </summary>
|
||||
[ColumnMapping("authHash")]
|
||||
public string PasswordHash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Authentication salt.
|
||||
/// </summary>
|
||||
[ColumnMapping("authSalt")]
|
||||
public string PasswordSalt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ip address of the creator.
|
||||
/// </summary>
|
||||
[ColumnMapping("creatorIp")]
|
||||
public string CreatorIp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the user last logged in.
|
||||
/// </summary>
|
||||
[ColumnMapping("lastLoggedIn")]
|
||||
public DateTime LastLoggedIn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's role.
|
||||
/// </summary>
|
||||
[ColumnMapping("role")]
|
||||
public string UserRole { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Group for this user's connections.
|
||||
/// </summary>
|
||||
[ColumnMapping("connectionGroup")]
|
||||
public Guid ConnectionGroup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Group for this user's friends.
|
||||
/// </summary>
|
||||
[ColumnMapping("friendGroup")]
|
||||
public Guid FriendsGroup { get; set; }
|
||||
}
|
57
Astral.Core/Entities/UserGroup.cs
Normal file
57
Astral.Core/Entities/UserGroup.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
// <copyright file="UserGroup.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Attributes.EntityAnnotation;
|
||||
|
||||
namespace Astral.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// User group entity.
|
||||
/// </summary>
|
||||
[TableMapping("userGroups")]
|
||||
public class UserGroup
|
||||
{
|
||||
/// <summary>
|
||||
/// User id.
|
||||
/// </summary>
|
||||
[ColumnMapping("id")]
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the user was created.
|
||||
/// </summary>
|
||||
[ColumnMapping("createdAt")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the user's entity was last updated.
|
||||
/// </summary>
|
||||
[ColumnMapping("updatedAt")]
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this group is an internal group.
|
||||
/// </summary>
|
||||
[ColumnMapping("internal")]
|
||||
public bool Internal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Owner user id.
|
||||
/// </summary>
|
||||
[ColumnMapping("ownerUserId")]
|
||||
public Guid OwnerUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Group title.
|
||||
/// </summary>
|
||||
[ColumnMapping("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Group description.
|
||||
/// </summary>
|
||||
[ColumnMapping("description")]
|
||||
public string Description { get; set; }
|
||||
}
|
45
Astral.Core/Entities/UserProfile.cs
Normal file
45
Astral.Core/Entities/UserProfile.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
// <copyright file="UserProfile.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Attributes.EntityAnnotation;
|
||||
|
||||
namespace Astral.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// User profile entity.
|
||||
/// </summary>
|
||||
[TableMapping("userProfile")]
|
||||
public class UserProfile
|
||||
{
|
||||
/// <summary>
|
||||
/// User id.
|
||||
/// </summary>
|
||||
[ColumnMapping("id")]
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the user was created.
|
||||
/// </summary>
|
||||
[ColumnMapping("createdAt")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the user's entity was last updated.
|
||||
/// </summary>
|
||||
[ColumnMapping("updatedAt")]
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hero image url.
|
||||
/// </summary>
|
||||
[ColumnMapping("heroImageUrl")]
|
||||
public string HeroImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Thumbnail image url.
|
||||
/// </summary>
|
||||
[ColumnMapping("thumbnailImageUrl")]
|
||||
public string ThumbnailImageUrl { get; set; }
|
||||
}
|
21
Astral.Core/Exceptions/MigrationException.cs
Normal file
21
Astral.Core/Exceptions/MigrationException.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
// <copyright file="MigrationException.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Core.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Exception to be thrown if errors occur during migrations.
|
||||
/// </summary>
|
||||
public class MigrationException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MigrationException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="file">The migration file causing the error.</param>
|
||||
/// <param name="error">The error message.</param>
|
||||
public MigrationException(string file, string error)
|
||||
: base($"An exception occured whilst attempting to migrate the database with file {file}: {error}")
|
||||
{
|
||||
}
|
||||
}
|
26
Astral.Core/Exceptions/MissingColumnAttribute.cs
Normal file
26
Astral.Core/Exceptions/MissingColumnAttribute.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
// <copyright file="MissingColumnAttribute.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Core.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown if an entity has a property that has a primary key attribute, but no column attribute.
|
||||
/// </summary>
|
||||
public class MissingColumnAttributeException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MissingColumnAttributeException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="entityType">The entity causing the exception.</param>
|
||||
public MissingColumnAttributeException(Type entityType)
|
||||
: base($"Entity '{entityType}' primary key requires a ColumnMapping attribute")
|
||||
{
|
||||
EntityType = entityType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The entity type.
|
||||
/// </summary>
|
||||
public Type EntityType { get; set; }
|
||||
}
|
26
Astral.Core/Exceptions/MultiplePrimaryKeysException.cs
Normal file
26
Astral.Core/Exceptions/MultiplePrimaryKeysException.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
// <copyright file="MultiplePrimaryKeysException.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Core.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown if an entity has multiple primary keys defined.
|
||||
/// </summary>
|
||||
public class MultiplePrimaryKeysException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MultiplePrimaryKeysException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="entityType">The entity causing the exception.</param>
|
||||
public MultiplePrimaryKeysException(Type entityType)
|
||||
: base($"Entity '{entityType}' contains multiple primary keys")
|
||||
{
|
||||
EntityType = entityType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The entity type.
|
||||
/// </summary>
|
||||
public Type EntityType { get; set; }
|
||||
}
|
28
Astral.Core/Exceptions/PrimaryKeyMissingException.cs
Normal file
28
Astral.Core/Exceptions/PrimaryKeyMissingException.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
// <copyright file="PrimaryKeyMissingException.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Attributes.EntityAnnotation;
|
||||
|
||||
namespace Astral.Core.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown if an entity is missing a property with <see cref="PrimaryKeyAttribute" />.
|
||||
/// </summary>
|
||||
public class PrimaryKeyMissingException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PrimaryKeyMissingException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="entityType">The entity causing the exception.</param>
|
||||
public PrimaryKeyMissingException(Type entityType)
|
||||
: base($"Entity '{entityType}' is missing a property with a PrimaryKey.")
|
||||
{
|
||||
EntityType = entityType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The entity type.
|
||||
/// </summary>
|
||||
public Type EntityType { get; set; }
|
||||
}
|
42
Astral.Core/Exceptions/ServiceExceptions.cs
Normal file
42
Astral.Core/Exceptions/ServiceExceptions.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
// <copyright file="ServiceExceptions.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Net;
|
||||
|
||||
namespace Astral.Core.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Service exception to be fed back into the response.
|
||||
/// </summary>
|
||||
public class ServiceException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServiceException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="errorCode">Error code to provide.</param>
|
||||
/// <param name="errorMessage">Error message to provide.</param>
|
||||
/// <param name="httpStatusCode">The http status code to provide.</param>
|
||||
protected ServiceException(string errorCode, string errorMessage, HttpStatusCode httpStatusCode)
|
||||
: base(errorMessage)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
ErrorMessage = errorMessage;
|
||||
HttpStatusCode = httpStatusCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Standard error code.
|
||||
/// </summary>
|
||||
public string ErrorCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message.
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Http Status Code.
|
||||
/// </summary>
|
||||
public HttpStatusCode HttpStatusCode { get; }
|
||||
}
|
25
Astral.Core/Exceptions/UnexpectedErrorException.cs
Normal file
25
Astral.Core/Exceptions/UnexpectedErrorException.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
// <copyright file="UnexpectedErrorException.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Net;
|
||||
using Astral.Core.Constants;
|
||||
|
||||
namespace Astral.Core.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when something bad has happened.
|
||||
/// </summary>
|
||||
public class UnexpectedErrorException : ServiceException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UnexpectedErrorException" /> class.
|
||||
/// </summary>
|
||||
public UnexpectedErrorException()
|
||||
: base(
|
||||
CoreErrorCodes.UnexpectedError,
|
||||
"Something unexpected occured, please try later",
|
||||
HttpStatusCode.InternalServerError)
|
||||
{
|
||||
}
|
||||
}
|
34
Astral.Core/Extensions/JwtConfigurationExtensions.cs
Normal file
34
Astral.Core/Extensions/JwtConfigurationExtensions.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
// <copyright file="JwtConfigurationExtensions.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Options;
|
||||
using TimeSpanParserUtil;
|
||||
|
||||
namespace Astral.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="JwtOptions" />.
|
||||
/// </summary>
|
||||
public static class JwtConfigurationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Return when (UTC) the access token expires.
|
||||
/// </summary>
|
||||
/// <param name="configuration">Instance of <see cref="JwtOptions" />.</param>
|
||||
/// <returns>A UTC DateTime of when the expiration occurs.</returns>
|
||||
public static DateTime AccessTokenExpirationDateTime(this JwtOptions configuration)
|
||||
{
|
||||
return DateTime.UtcNow.Add(TimeSpanParser.Parse(configuration.AccessTokenExpiration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return when (UTC) the refresh token expires.
|
||||
/// </summary>
|
||||
/// <param name="configuration">Instance of <see cref="JwtOptions" />.</param>
|
||||
/// <returns>A UTC DateTime of when the expiration occurs.</returns>
|
||||
public static DateTime RefreshTokenExpirationDateTime(this JwtOptions configuration)
|
||||
{
|
||||
return DateTime.UtcNow.Add(TimeSpanParser.Parse(configuration.RefreshTokenExpiration));
|
||||
}
|
||||
}
|
27
Astral.Core/Extensions/ListExtensions.cs
Normal file
27
Astral.Core/Extensions/ListExtensions.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
// <copyright file="ListExtensions.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for lists.
|
||||
/// </summary>
|
||||
public static class ListExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Chunk a list into smaller chunks.
|
||||
/// </summary>
|
||||
/// <param name="source">Source list.</param>
|
||||
/// <param name="chunkSize">Chunk size.</param>
|
||||
/// <typeparam name="T">List type.</typeparam>
|
||||
/// <returns>Collection of chunked lists.</returns>
|
||||
public static List<List<T>> ChunkBy<T>(this List<T> source, int chunkSize)
|
||||
{
|
||||
return source
|
||||
.Select((x, i) => new { Index = i, Value = x })
|
||||
.GroupBy(x => x.Index / chunkSize)
|
||||
.Select(x => x.Select(v => v.Value).ToList())
|
||||
.ToList();
|
||||
}
|
||||
}
|
36
Astral.Core/Extensions/StringExtensions.cs
Normal file
36
Astral.Core/Extensions/StringExtensions.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
// <copyright file="StringExtensions.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for strings.
|
||||
/// </summary>
|
||||
public static class StringExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Mask an email address for logging purposes.
|
||||
/// </summary>
|
||||
/// <param name="emailAddress">The email address to obfuscate.</param>
|
||||
/// <returns>The obfuscated email address, or the original if there's an issue with the email address.</returns>
|
||||
public static string MaskEmailAddress(this string emailAddress)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(emailAddress))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (!emailAddress.Contains('@'))
|
||||
{
|
||||
return emailAddress;
|
||||
}
|
||||
|
||||
if (emailAddress.StartsWith('@'))
|
||||
{
|
||||
return emailAddress;
|
||||
}
|
||||
|
||||
return $"{emailAddress[0]}****{emailAddress[(emailAddress.IndexOf('@') - 1) ..]}";
|
||||
}
|
||||
}
|
16
Astral.Core/Infrastructure/IDataAccessLayerSetup.cs
Normal file
16
Astral.Core/Infrastructure/IDataAccessLayerSetup.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
// <copyright file="IDataAccessLayerSetup.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Core.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Perform any setup steps required for the data access layer.
|
||||
/// </summary>
|
||||
public interface IDataAccessLayerSetup
|
||||
{
|
||||
/// <summary>
|
||||
/// Setup routine.
|
||||
/// </summary>
|
||||
Task Setup();
|
||||
}
|
17
Astral.Core/Infrastructure/IDatabaseMigrator.cs
Normal file
17
Astral.Core/Infrastructure/IDatabaseMigrator.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
// <copyright file="IDatabaseMigrator.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Core.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Database migration process.
|
||||
/// </summary>
|
||||
public interface IDatabaseMigrator
|
||||
{
|
||||
/// <summary>
|
||||
/// Run database migrations.
|
||||
/// </summary>
|
||||
/// <param name="migrationsPath">The directory where migrations are found.</param>
|
||||
Task MigrateDatabaseAsync(string migrationsPath = "Migrations");
|
||||
}
|
25
Astral.Core/Infrastructure/IDbConnectionProvider.cs
Normal file
25
Astral.Core/Infrastructure/IDbConnectionProvider.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
// <copyright file="IDbConnectionProvider.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Data;
|
||||
|
||||
namespace Astral.Core.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Scoped database connection provider.
|
||||
/// </summary>
|
||||
public interface IDbConnectionProvider : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Open new database connection for this scope.
|
||||
/// </summary>
|
||||
/// <returns>Instance of <see cref="IDbConnection" />.</returns>
|
||||
IDbConnection OpenConnection();
|
||||
|
||||
/// <summary>
|
||||
/// Open new database connection for this scope.
|
||||
/// </summary>
|
||||
/// <returns>Instance of <see cref="IDbConnection" />.</returns>
|
||||
Task<IDbConnection> OpenConnectionAsync();
|
||||
}
|
27
Astral.Core/Infrastructure/ITransactionProvider.cs
Normal file
27
Astral.Core/Infrastructure/ITransactionProvider.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
// <copyright file="ITransactionProvider.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Data;
|
||||
|
||||
namespace Astral.Core.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Database transaction provider.
|
||||
/// </summary>
|
||||
public interface ITransactionProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Begin a new transaction.
|
||||
/// </summary>
|
||||
/// <param name="isolationLevel">One of <see cref="IsolationLevel" />.</param>
|
||||
/// <returns>Instance of <see cref="IDbTransaction" />.</returns>
|
||||
IDbTransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified);
|
||||
|
||||
/// <summary>
|
||||
/// Begin a new transaction.
|
||||
/// </summary>
|
||||
/// <param name="isolationLevel">One of <see cref="IsolationLevel" />.</param>
|
||||
/// <returns>Instance of <see cref="IDbTransaction" />.</returns>
|
||||
Task<IDbTransaction> BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified);
|
||||
}
|
16
Astral.Core/Options/DatabaseOptions.cs
Normal file
16
Astral.Core/Options/DatabaseOptions.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
// <copyright file="DatabaseOptions.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Core.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Database Options.
|
||||
/// </summary>
|
||||
public class DatabaseOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Connection string.
|
||||
/// </summary>
|
||||
public string ConnectionString { get; init; }
|
||||
}
|
46
Astral.Core/Options/JwtOptions.cs
Normal file
46
Astral.Core/Options/JwtOptions.cs
Normal file
|
@ -0,0 +1,46 @@
|
|||
// <copyright file="JwtOptions.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Core.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Jwt Authentication Configuration.
|
||||
/// </summary>
|
||||
public class JwtOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Secret key.
|
||||
/// </summary>
|
||||
public string SecretKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Issuer.
|
||||
/// </summary>
|
||||
public string Issuer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Audience.
|
||||
/// </summary>
|
||||
public string Audience { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Access token expiration.
|
||||
/// </summary>
|
||||
public string AccessTokenExpiration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Refresh token expiration.
|
||||
/// </summary>
|
||||
public string RefreshTokenExpiration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Validate issuer.
|
||||
/// </summary>
|
||||
public bool ValidateIssuer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Validate audience.
|
||||
/// </summary>
|
||||
public bool ValidateAudience { get; set; }
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// <copyright file="IEmailDomainBlacklistRepository.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Entities;
|
||||
|
||||
namespace Astral.Core.RepositoryInterfaces;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="EmailDomainBlacklist" /> Repository.
|
||||
/// </summary>
|
||||
public interface IEmailDomainBlacklistRepository : IGenericRepository<EmailDomainBlacklist, string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Count how many domains are in the blacklist.
|
||||
/// </summary>
|
||||
/// <returns>A count of entries.</returns>
|
||||
Task<long> CountEntries();
|
||||
}
|
54
Astral.Core/RepositoryInterfaces/IGenericRepository.cs
Normal file
54
Astral.Core/RepositoryInterfaces/IGenericRepository.cs
Normal file
|
@ -0,0 +1,54 @@
|
|||
// <copyright file="IGenericRepository.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Core.RepositoryInterfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Generic repository with auto-generated CRUD operations.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">The entity this repository handles.</typeparam>
|
||||
/// <typeparam name="TKeyType">The data type of the primary key.</typeparam>
|
||||
public interface IGenericRepository<TEntity, TKeyType>
|
||||
where TEntity : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Find an entity by its id.
|
||||
/// </summary>
|
||||
/// <param name="id">The entity's id.</param>
|
||||
/// <returns>The entity, or null if not found.</returns>
|
||||
Task<TEntity> FindByIdAsync(TKeyType id);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all entities.
|
||||
/// </summary>
|
||||
/// <returns>Collection of entities.</returns>
|
||||
Task<IEnumerable<TEntity>> GetAllAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Add an entity to the table and return it's new id.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity to add.</param>
|
||||
/// <returns>The id of the new entity.</returns>
|
||||
Task<TKeyType> AddAsync(TEntity entity);
|
||||
|
||||
/// <summary>
|
||||
/// Add multiple entities to the table.
|
||||
/// </summary>
|
||||
/// <param name="entities">The entities to add.</param>
|
||||
/// <returns>The id of the new entity.</returns>
|
||||
Task AddAsync(IEnumerable<TEntity> entities);
|
||||
|
||||
/// <summary>
|
||||
/// Update an entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity (needs an id) to update.</param>
|
||||
/// <returns>True if entity updated.</returns>
|
||||
Task<bool> UpdateAsync(TEntity entity);
|
||||
|
||||
/// <summary>
|
||||
/// Delete an entity by its id.
|
||||
/// </summary>
|
||||
/// <param name="id">The entity id.</param>
|
||||
Task DeleteAsync(TKeyType id);
|
||||
}
|
14
Astral.Core/RepositoryInterfaces/IUserGroupRepository.cs
Normal file
14
Astral.Core/RepositoryInterfaces/IUserGroupRepository.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
// <copyright file="IUserGroupRepository.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Entities;
|
||||
|
||||
namespace Astral.Core.RepositoryInterfaces;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="UserGroup" /> repository.
|
||||
/// </summary>
|
||||
public interface IUserGroupRepository : IGenericRepository<UserGroup, Guid>
|
||||
{
|
||||
}
|
14
Astral.Core/RepositoryInterfaces/IUserProfileRepository.cs
Normal file
14
Astral.Core/RepositoryInterfaces/IUserProfileRepository.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
// <copyright file="IUserProfileRepository.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Entities;
|
||||
|
||||
namespace Astral.Core.RepositoryInterfaces;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="UserProfile" /> repository.
|
||||
/// </summary>
|
||||
public interface IUserProfileRepository : IGenericRepository<UserProfile, Guid>
|
||||
{
|
||||
}
|
33
Astral.Core/RepositoryInterfaces/IUserRepository.cs
Normal file
33
Astral.Core/RepositoryInterfaces/IUserRepository.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
// <copyright file="IUserRepository.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Entities;
|
||||
|
||||
namespace Astral.Core.RepositoryInterfaces;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="User" /> repository.
|
||||
/// </summary>
|
||||
public interface IUserRepository : IGenericRepository<User, Guid>
|
||||
{
|
||||
/// <summary>
|
||||
/// Return a count of all users.
|
||||
/// </summary>
|
||||
/// <returns>A count of users in the table.</returns>
|
||||
Task<long> CountUsersAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Find a user by their username.
|
||||
/// </summary>
|
||||
/// <param name="username">The username to search for.</param>
|
||||
/// <returns>The user entity if found, or null.</returns>
|
||||
Task<User> FindByUsername(string username);
|
||||
|
||||
/// <summary>
|
||||
/// Find a user by their email address.
|
||||
/// </summary>
|
||||
/// <param name="emailAddress">The email address to search for.</param>
|
||||
/// <returns>The user entity if found, or null.</returns>
|
||||
Task<User> FindByEmailAddress(string emailAddress);
|
||||
}
|
30
Astral.DAL/Astral.DAL.csproj
Normal file
30
Astral.DAL/Astral.DAL.csproj
Normal file
|
@ -0,0 +1,30 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<CodeAnalysisRuleSet>../Alveus.ruleset</CodeAnalysisRuleSet>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Astral.Core\Astral.Core.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.35"/>
|
||||
<PackageReference Include="Npgsql" Version="9.0.2"/>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Migrations\*">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
54
Astral.DAL/Infrastructure/DataAccessLayerSetup.cs
Normal file
54
Astral.DAL/Infrastructure/DataAccessLayerSetup.cs
Normal file
|
@ -0,0 +1,54 @@
|
|||
// <copyright file="DataAccessLayerSetup.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Attributes.EntityAnnotation;
|
||||
using Astral.Core.Infrastructure;
|
||||
using Dapper;
|
||||
using Injectio.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Astral.DAL.Infrastructure;
|
||||
|
||||
/// <inheritdoc />
|
||||
[RegisterScoped]
|
||||
public class DataAccessLayerSetup : IDataAccessLayerSetup
|
||||
{
|
||||
private readonly ILogger<DataAccessLayerSetup> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataAccessLayerSetup" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of <see cref="ILogger" />.</param>
|
||||
public DataAccessLayerSetup(ILogger<DataAccessLayerSetup> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task Setup()
|
||||
{
|
||||
_logger.LogInformation("Setting up data access layer");
|
||||
|
||||
DefaultTypeMap.MatchNamesWithUnderscores = true;
|
||||
|
||||
SetupMappings();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup custom mapping on all entities decorated with <see cref="TableMappingAttribute" />.
|
||||
/// </summary>
|
||||
private static void SetupMappings()
|
||||
{
|
||||
// Find all entities annotated with Table attribute.
|
||||
var entityTypes = typeof(IDataAccessLayerSetup).Assembly.GetTypes()
|
||||
.Where(type => type.GetCustomAttributes(typeof(TableMappingAttribute), true).Any());
|
||||
|
||||
foreach (var entity in entityTypes)
|
||||
{
|
||||
SqlMapper.SetTypeMap(entity, new EntityAttributeTypeMapper(entity));
|
||||
}
|
||||
}
|
||||
}
|
95
Astral.DAL/Infrastructure/DatabaseMigrator.cs
Normal file
95
Astral.DAL/Infrastructure/DatabaseMigrator.cs
Normal file
|
@ -0,0 +1,95 @@
|
|||
// <copyright file="DatabaseMigrator.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Exceptions;
|
||||
using Astral.Core.Infrastructure;
|
||||
using Dapper;
|
||||
using Injectio.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Astral.DAL.Infrastructure;
|
||||
|
||||
/// <inheritdoc />
|
||||
[RegisterScoped]
|
||||
public class DatabaseMigrator : IDatabaseMigrator
|
||||
{
|
||||
private readonly IDbConnectionProvider _dbConnectionProvider;
|
||||
private readonly ILogger<DatabaseMigrator> _logger;
|
||||
private readonly ITransactionProvider _transactionProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DatabaseMigrator" /> class.
|
||||
/// </summary>
|
||||
/// <param name="dbConnectionProvider">Instance of <see cref="IDbConnectionProvider" />.</param>
|
||||
/// <param name="transactionProvider">Instance of <see cref="ITransactionProvider" />.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger" />.</param>
|
||||
public DatabaseMigrator(
|
||||
IDbConnectionProvider dbConnectionProvider,
|
||||
ITransactionProvider transactionProvider,
|
||||
ILogger<DatabaseMigrator> logger)
|
||||
{
|
||||
_dbConnectionProvider = dbConnectionProvider;
|
||||
_transactionProvider = transactionProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task MigrateDatabaseAsync(string migrationsPath)
|
||||
{
|
||||
var connection = await _dbConnectionProvider.OpenConnectionAsync();
|
||||
|
||||
_logger.LogInformation("Beginning database migrations");
|
||||
using var transaction = await _transactionProvider.BeginTransactionAsync();
|
||||
|
||||
await connection.ExecuteAsync("""
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
date_added TIMESTAMP NOT NULL,
|
||||
filename VARCHAR(128) NOT NULL,
|
||||
CONSTRAINT migrations_filename_pk PRIMARY KEY (filename)
|
||||
);
|
||||
""");
|
||||
|
||||
var appliedMigrationsEnumerable = await
|
||||
connection.QueryAsync<string>("SELECT filename FROM migrations ORDER BY date_added;");
|
||||
|
||||
// Prevent multiple enumeration.
|
||||
var appliedMigrations = appliedMigrationsEnumerable.ToList();
|
||||
|
||||
var files = Directory.GetFiles(migrationsPath, "*.sql").ToList();
|
||||
|
||||
files.Sort();
|
||||
foreach (var filename in files)
|
||||
{
|
||||
var file = filename.Replace(migrationsPath, string.Empty)
|
||||
.Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
if (appliedMigrations.Contains(file))
|
||||
{
|
||||
_logger.LogInformation("Already applied: {file}", file);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Applying {file}...", file);
|
||||
try
|
||||
{
|
||||
var sql = await File.ReadAllTextAsync(filename);
|
||||
await connection.ExecuteAsync(sql);
|
||||
|
||||
await connection.ExecuteAsync(
|
||||
"INSERT INTO migrations (date_added, filename) SELECT CURRENT_TIMESTAMP, @pFilename ;",
|
||||
new
|
||||
{
|
||||
pFilename = file
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new MigrationException(file, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
|
||||
_logger.LogInformation("Database migrations complete");
|
||||
}
|
||||
}
|
99
Astral.DAL/Infrastructure/DbConnectionProvider.cs
Normal file
99
Astral.DAL/Infrastructure/DbConnectionProvider.cs
Normal file
|
@ -0,0 +1,99 @@
|
|||
// <copyright file="DbConnectionProvider.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Data;
|
||||
using Astral.Core.Infrastructure;
|
||||
using Astral.Core.Options;
|
||||
using Injectio.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Npgsql;
|
||||
|
||||
namespace Astral.DAL.Infrastructure;
|
||||
|
||||
/// <inheritdoc cref="Astral.Core.Infrastructure.IDbConnectionProvider" />
|
||||
[RegisterScoped]
|
||||
public class DbConnectionProvider : IDbConnectionProvider, IAsyncDisposable
|
||||
{
|
||||
private readonly IOptions<DatabaseOptions> _databaseConfiguration;
|
||||
private readonly ILogger<DbConnectionProvider> _logger;
|
||||
private NpgsqlConnection _dbConnection;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DbConnectionProvider" /> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseConfiguration">Instance of <see cref="IOptions{DatabaseOptions}" />.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger" />.</param>
|
||||
public DbConnectionProvider(
|
||||
IOptions<DatabaseOptions> databaseConfiguration,
|
||||
ILogger<DbConnectionProvider> logger)
|
||||
{
|
||||
_databaseConfiguration = databaseConfiguration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_dbConnection != null)
|
||||
{
|
||||
await _dbConnection.DisposeAsync();
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IDbConnectionProvider" />
|
||||
public IDbConnection OpenConnection()
|
||||
{
|
||||
if (_dbConnection is not null)
|
||||
{
|
||||
if (_dbConnection.State == ConnectionState.Closed)
|
||||
{
|
||||
_dbConnection.Open();
|
||||
}
|
||||
|
||||
return _dbConnection;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Opening database connection");
|
||||
_dbConnection = new NpgsqlConnection(_databaseConfiguration.Value.ConnectionString);
|
||||
_dbConnection.Open();
|
||||
return _dbConnection;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IDbConnection> OpenConnectionAsync()
|
||||
{
|
||||
if (_dbConnection is not null)
|
||||
{
|
||||
if (_dbConnection.State == ConnectionState.Closed)
|
||||
{
|
||||
await _dbConnection.OpenAsync();
|
||||
}
|
||||
|
||||
return _dbConnection;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Opening database connection");
|
||||
_dbConnection = new NpgsqlConnection(_databaseConfiguration.Value.ConnectionString);
|
||||
await _dbConnection.OpenAsync();
|
||||
return _dbConnection;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_dbConnection is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Disposing database connection");
|
||||
_dbConnection.Close();
|
||||
_dbConnection.Dispose();
|
||||
_dbConnection = null;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
35
Astral.DAL/Infrastructure/EntityAttributeTypeMapper.cs
Normal file
35
Astral.DAL/Infrastructure/EntityAttributeTypeMapper.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
// <copyright file="EntityAttributeTypeMapper.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Reflection;
|
||||
using Astral.Core.Attributes.EntityAnnotation;
|
||||
using Dapper;
|
||||
|
||||
namespace Astral.DAL.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Entity attribute type mapper.
|
||||
/// </summary>
|
||||
public class EntityAttributeTypeMapper : FallbackTypeMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EntityAttributeTypeMapper" /> class.
|
||||
/// </summary>
|
||||
/// <param name="entityType">The entity type.</param>
|
||||
public EntityAttributeTypeMapper(Type entityType)
|
||||
: base([
|
||||
new CustomPropertyTypeMap(
|
||||
entityType,
|
||||
(type, columnName) =>
|
||||
type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
.FirstOrDefault(
|
||||
prop =>
|
||||
prop.GetCustomAttributes(false)
|
||||
.OfType<ColumnMappingAttribute>()
|
||||
.Any(attr => attr.Name.Equals(columnName, StringComparison.OrdinalIgnoreCase)))),
|
||||
new DefaultTypeMap(entityType)
|
||||
])
|
||||
{
|
||||
}
|
||||
}
|
114
Astral.DAL/Infrastructure/FallbackTypeMapper.cs
Normal file
114
Astral.DAL/Infrastructure/FallbackTypeMapper.cs
Normal file
|
@ -0,0 +1,114 @@
|
|||
// <copyright file="FallbackTypeMapper.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Reflection;
|
||||
using Dapper;
|
||||
|
||||
namespace Astral.DAL.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Fallback type mapper.
|
||||
/// </summary>
|
||||
public class FallbackTypeMapper : SqlMapper.ITypeMap
|
||||
{
|
||||
private readonly IEnumerable<SqlMapper.ITypeMap> _mappers;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FallbackTypeMapper" /> class.
|
||||
/// </summary>
|
||||
/// <param name="mappers">Collection of <see cref="SqlMapper.ITypeMap" />.</param>
|
||||
protected FallbackTypeMapper(IEnumerable<SqlMapper.ITypeMap> mappers)
|
||||
{
|
||||
_mappers = mappers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find constructor.
|
||||
/// </summary>
|
||||
/// <param name="names">Collection of names.</param>
|
||||
/// <param name="types">Collection of types.</param>
|
||||
/// <returns>Instance of <see cref="ConstructorInfo" />.</returns>
|
||||
public ConstructorInfo FindConstructor(string[] names, Type[] types)
|
||||
{
|
||||
foreach (var mapper in _mappers)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = mapper.FindConstructor(names, types);
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find explicit constructor.
|
||||
/// </summary>
|
||||
/// <returns>Instance of <see cref="ConstructorInfo" />.</returns>
|
||||
public ConstructorInfo FindExplicitConstructor()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get constructor parameter map.
|
||||
/// </summary>
|
||||
/// <param name="constructor">Instance of <see cref="ConstructorInfo" />.</param>
|
||||
/// <param name="columnName">Column name.</param>
|
||||
/// <returns>Instance of <see cref="SqlMapper.IMemberMap" />.</returns>
|
||||
public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName)
|
||||
{
|
||||
foreach (var mapper in _mappers)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = mapper.GetConstructorParameter(constructor, columnName);
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get member.
|
||||
/// </summary>
|
||||
/// <param name="columnName">Column name.</param>
|
||||
/// <returns>Instance of <see cref="SqlMapper.IMemberMap" />.</returns>
|
||||
public SqlMapper.IMemberMap GetMember(string columnName)
|
||||
{
|
||||
foreach (var mapper in _mappers)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = mapper.GetMember(columnName);
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
39
Astral.DAL/Infrastructure/TransactionProvider.cs
Normal file
39
Astral.DAL/Infrastructure/TransactionProvider.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
// <copyright file="TransactionProvider.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Data;
|
||||
using Astral.Core.Infrastructure;
|
||||
using Injectio.Attributes;
|
||||
|
||||
namespace Astral.DAL.Infrastructure;
|
||||
|
||||
/// <inheritdoc />
|
||||
[RegisterScoped]
|
||||
public class TransactionProvider : ITransactionProvider
|
||||
{
|
||||
private readonly IDbConnectionProvider _dbConnectionProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TransactionProvider" /> class.
|
||||
/// </summary>
|
||||
/// <param name="dbConnectionProvider">Instance of <see cref="IDbConnectionProvider" />.</param>
|
||||
public TransactionProvider(IDbConnectionProvider dbConnectionProvider)
|
||||
{
|
||||
_dbConnectionProvider = dbConnectionProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ITransactionProvider" />
|
||||
public IDbTransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified)
|
||||
{
|
||||
var connection = _dbConnectionProvider.OpenConnection();
|
||||
return connection.BeginTransaction(isolationLevel);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ITransactionProvider" />
|
||||
public async Task<IDbTransaction> BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified)
|
||||
{
|
||||
var connection = await _dbConnectionProvider.OpenConnectionAsync();
|
||||
return connection.BeginTransaction(isolationLevel);
|
||||
}
|
||||
}
|
9
Astral.DAL/Migrations/2024-12-09.01-initial.sql
Normal file
9
Astral.DAL/Migrations/2024-12-09.01-initial.sql
Normal file
|
@ -0,0 +1,9 @@
|
|||
CREATE OR REPLACE FUNCTION updated_at_timestamp() RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS
|
||||
$$
|
||||
BEGIN
|
||||
NEW.updatedAt = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
|
@ -0,0 +1,5 @@
|
|||
CREATE TABLE emailDomainBlacklist
|
||||
(
|
||||
domain TEXT UNIQUE PRIMARY KEY NOT NULL,
|
||||
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
170423
Astral.DAL/Migrations/2024-12-09.03-emailDomainBlacklist-seed.sql
Normal file
170423
Astral.DAL/Migrations/2024-12-09.03-emailDomainBlacklist-seed.sql
Normal file
File diff suppressed because it is too large
Load diff
23
Astral.DAL/Migrations/2024-12-09.04-users.sql
Normal file
23
Astral.DAL/Migrations/2024-12-09.04-users.sql
Normal file
|
@ -0,0 +1,23 @@
|
|||
CREATE TABLE users
|
||||
(
|
||||
id UUID UNIQUE PRIMARY KEY NOT NULL,
|
||||
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
email TEXT UNIQUE,
|
||||
authHash TEXT,
|
||||
authSalt TEXT,
|
||||
lastLoggedIn TIMESTAMP DEFAULT NULL,
|
||||
creatorIp TEXT DEFAULT '0.0.0.0',
|
||||
role TEXT DEFAULT 'user' NOT NULL,
|
||||
state SMALLINT DEFAULT 0 NOT NULL,
|
||||
connectionGroup UUID DEFAULT NULL,
|
||||
friendGroup UUID DEFAULT NULL,
|
||||
language TEXT DEFAULT 'en'
|
||||
);
|
||||
|
||||
CREATE TRIGGER users_updated_at
|
||||
BEFORE UPDATE
|
||||
ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_timestamp();
|
14
Astral.DAL/Migrations/2024-12-10.05-userProfile.sql
Normal file
14
Astral.DAL/Migrations/2024-12-10.05-userProfile.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
CREATE TABLE userProfile
|
||||
(
|
||||
id UUID UNIQUE PRIMARY KEY REFERENCES users (id) ON DELETE CASCADE,
|
||||
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
heroImageUrl TEXT DEFAULT '',
|
||||
thumbnailImageUrl TEXT DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TRIGGER userProfile_updated_at
|
||||
BEFORE UPDATE
|
||||
ON userProfile
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_timestamp();
|
16
Astral.DAL/Migrations/2024-12-11.01-userGroups.sql
Normal file
16
Astral.DAL/Migrations/2024-12-11.01-userGroups.sql
Normal file
|
@ -0,0 +1,16 @@
|
|||
CREATE TABLE userGroups
|
||||
(
|
||||
id UUID UNIQUE PRIMARY KEY NOT NULL,
|
||||
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
internal BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
ownerUserId UUID REFERENCES users (id) ON DELETE CASCADE NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT DEFAULT '' NOT NULL
|
||||
);
|
||||
|
||||
CREATE TRIGGER userGroups_updated_at
|
||||
BEFORE UPDATE
|
||||
ON userGroups
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_timestamp();
|
252
Astral.DAL/Repositories/BaseRepository.cs
Normal file
252
Astral.DAL/Repositories/BaseRepository.cs
Normal file
|
@ -0,0 +1,252 @@
|
|||
// <copyright file="BaseRepository.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Data;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Astral.Core.Attributes.EntityAnnotation;
|
||||
using Astral.Core.Exceptions;
|
||||
using Astral.Core.Infrastructure;
|
||||
using Astral.Core.RepositoryInterfaces;
|
||||
using Dapper;
|
||||
|
||||
namespace Astral.DAL.Repositories;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class BaseRepository<TEntity, TKeyType> : IGenericRepository<TEntity, TKeyType>
|
||||
where TEntity : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Instance of <see cref="IDbConnectionProvider" />.
|
||||
/// </summary>
|
||||
private readonly IDbConnectionProvider _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseRepository{TEntity, TKeyType}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="db">Instance of <see cref="IDbConnectionProvider" />.</param>
|
||||
protected BaseRepository(IDbConnectionProvider db)
|
||||
{
|
||||
_db = db;
|
||||
SetupRepository();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Table name.
|
||||
/// </summary>
|
||||
protected string Table { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Primary key column name.
|
||||
/// </summary>
|
||||
private string PrimaryKeyColumn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the primary key should be treated as auto-generated by the database.
|
||||
/// </summary>
|
||||
private bool AutoPrimaryKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Generated select all query string.
|
||||
/// </summary>
|
||||
private string SelectAllQuery { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Generated find by id query string.
|
||||
/// </summary>
|
||||
private string FindByIdQuery { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Generated insert query string.
|
||||
/// </summary>
|
||||
private string InsertQuery { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Generated update query string.
|
||||
/// </summary>
|
||||
private string UpdateQuery { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Generated delete query string.
|
||||
/// </summary>
|
||||
private string DeleteQuery { get; set; }
|
||||
|
||||
/// <inheritdoc cref="IGenericRepository{TEntity,TKeyType}" />
|
||||
public async Task<TEntity> FindByIdAsync(TKeyType id)
|
||||
{
|
||||
return await WithDatabaseAsync(async connection =>
|
||||
{
|
||||
var result = await connection.QueryFirstOrDefaultAsync<TEntity>(FindByIdQuery, new
|
||||
{
|
||||
pId = id
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IGenericRepository{TEntity,TKeyType}" />
|
||||
public async Task<IEnumerable<TEntity>> GetAllAsync()
|
||||
{
|
||||
return await WithDatabaseAsync(async connection =>
|
||||
{
|
||||
var result = await connection.QueryAsync<TEntity>(SelectAllQuery);
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IGenericRepository{TEntity,TKeyType}" />
|
||||
public async Task<TKeyType> AddAsync(TEntity entity)
|
||||
{
|
||||
return await WithDatabaseAsync(async connection => await connection.QuerySingleAsync<TKeyType>(
|
||||
InsertQuery, entity));
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IGenericRepository{TEntity,TKeyType}" />
|
||||
public async Task AddAsync(IEnumerable<TEntity> entities)
|
||||
{
|
||||
await WithDatabaseAsync(async connection =>
|
||||
{
|
||||
await connection.ExecuteAsync(
|
||||
InsertQuery, entities);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IGenericRepository{TEntity,TKeyType}" />
|
||||
public async Task<bool> UpdateAsync(TEntity entity)
|
||||
{
|
||||
return await WithDatabaseAsync(async connection => await connection.ExecuteAsync(UpdateQuery, entity) > 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IGenericRepository{TEntity,TKeyType}" />
|
||||
public async Task DeleteAsync(TKeyType id)
|
||||
{
|
||||
await WithDatabaseAsync(async connection => await connection.ExecuteAsync(DeleteQuery, new
|
||||
{
|
||||
pId = id
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Establish a connection and perform the query function.
|
||||
/// </summary>
|
||||
/// <param name="query">Query method to execute.</param>
|
||||
/// <typeparam name="T">The return type.</typeparam>
|
||||
/// <returns>Result of query.</returns>
|
||||
/// <exception cref="Exception">Thrown if an exception occurs.</exception>
|
||||
protected async Task<T> WithDatabaseAsync<T>(Func<IDbConnection, Task<T>> query)
|
||||
{
|
||||
var connection = await _db.OpenConnectionAsync();
|
||||
return await query(connection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Establish a connection and perform the query function.
|
||||
/// </summary>
|
||||
/// <param name="query">Query method to execute.</param>
|
||||
protected async Task WithDatabaseAsync(Func<IDbConnection, Task> query)
|
||||
{
|
||||
var connection = await _db.OpenConnectionAsync();
|
||||
await query(connection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetch a comma separated list of property names.
|
||||
/// </summary>
|
||||
/// <param name="excludeKey">True to exclude key property.</param>
|
||||
/// <returns>Comma separated list of column names.</returns>
|
||||
private static string GetPropertyNames(bool excludeKey = false)
|
||||
{
|
||||
var properties = typeof(TEntity).GetProperties()
|
||||
.Where(p => !excludeKey || p.GetCustomAttribute<PrimaryKeyAttribute>() == null)
|
||||
.Where(p => p.IsDefined(typeof(PrimaryKeyAttribute), true) ||
|
||||
p.IsDefined(typeof(ColumnMappingAttribute), true));
|
||||
|
||||
var values = string.Join(", ", properties.Select(p => $"@{p.Name}"));
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup repository.
|
||||
/// </summary>
|
||||
private void SetupRepository()
|
||||
{
|
||||
var entityType = typeof(TEntity);
|
||||
var properties = entityType.GetProperties();
|
||||
var propertiesExcludingKey = properties
|
||||
.Where(p => p.GetCustomAttribute<PrimaryKeyAttribute>() == null);
|
||||
|
||||
// Set up table name.
|
||||
var tableAttribute = entityType.GetCustomAttribute<TableMappingAttribute>();
|
||||
if (tableAttribute is null)
|
||||
{
|
||||
Table = typeof(TEntity).Name + "s";
|
||||
}
|
||||
else
|
||||
{
|
||||
Table = tableAttribute.Name;
|
||||
}
|
||||
|
||||
// Set up primary key.
|
||||
var primaryKeyProperties = properties.Where(p => p.IsDefined(typeof(PrimaryKeyAttribute), true)).ToList();
|
||||
|
||||
if (primaryKeyProperties is null || primaryKeyProperties.Count == 0)
|
||||
{
|
||||
throw new PrimaryKeyMissingException(typeof(TEntity));
|
||||
}
|
||||
|
||||
if (primaryKeyProperties.Count > 1)
|
||||
{
|
||||
throw new MultiplePrimaryKeysException(typeof(TEntity));
|
||||
}
|
||||
|
||||
var primaryKeyProperty = primaryKeyProperties.First();
|
||||
var primaryKey = primaryKeyProperty.GetCustomAttribute<PrimaryKeyAttribute>();
|
||||
var primaryKeyCol = primaryKeyProperty.GetCustomAttribute<ColumnMappingAttribute>();
|
||||
|
||||
if (primaryKeyCol is null)
|
||||
{
|
||||
throw new MissingColumnAttributeException(typeof(TEntity));
|
||||
}
|
||||
|
||||
PrimaryKeyColumn = primaryKeyCol!.Name;
|
||||
AutoPrimaryKey = primaryKey!.AutoGenerated;
|
||||
|
||||
SelectAllQuery = $"SELECT * FROM {Table};";
|
||||
FindByIdQuery = $"SELECT * FROM {Table} WHERE {PrimaryKeyColumn} = @pId;";
|
||||
|
||||
// Add query.
|
||||
var columns = string.Join(", ", properties
|
||||
.Where(p => !AutoPrimaryKey || !p.IsDefined(typeof(PrimaryKeyAttribute)))
|
||||
.Where(p => p.IsDefined(typeof(ColumnMappingAttribute)))
|
||||
.Select(p => p.GetCustomAttribute<ColumnMappingAttribute>() !.Name));
|
||||
|
||||
var propertyNames = GetPropertyNames(AutoPrimaryKey);
|
||||
|
||||
var insertQuery = new StringBuilder();
|
||||
insertQuery.Append(
|
||||
$"INSERT INTO {Table} ({columns}) VALUES ({propertyNames}) RETURNING {PrimaryKeyColumn};");
|
||||
|
||||
InsertQuery = insertQuery.ToString();
|
||||
|
||||
// Update query.
|
||||
var updateQuery = new StringBuilder();
|
||||
updateQuery.Append($"UPDATE {Table} SET");
|
||||
foreach (var property in propertiesExcludingKey.Where(p => p.IsDefined(typeof(ColumnMappingAttribute))))
|
||||
{
|
||||
var columnAttr = property.GetCustomAttribute<ColumnMappingAttribute>();
|
||||
var propertyName = property.Name;
|
||||
updateQuery.Append($" {columnAttr!.Name} = @{propertyName},");
|
||||
}
|
||||
|
||||
updateQuery.Remove(updateQuery.Length - 1, 1);
|
||||
updateQuery.Append($" WHERE {PrimaryKeyColumn} = @{primaryKeyProperty.Name};");
|
||||
UpdateQuery = updateQuery.ToString();
|
||||
|
||||
// Delete query.
|
||||
DeleteQuery = $"DELETE FROM {Table} WHERE {PrimaryKeyColumn} = @pId;";
|
||||
}
|
||||
}
|
33
Astral.DAL/Repositories/EmailDomainBlacklistRepository.cs
Normal file
33
Astral.DAL/Repositories/EmailDomainBlacklistRepository.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
// <copyright file="EmailDomainBlacklistRepository.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Entities;
|
||||
using Astral.Core.Infrastructure;
|
||||
using Astral.Core.RepositoryInterfaces;
|
||||
using Dapper;
|
||||
using Injectio.Attributes;
|
||||
|
||||
namespace Astral.DAL.Repositories;
|
||||
|
||||
/// <inheritdoc cref="IEmailDomainBlacklistRepository" />
|
||||
[RegisterScoped]
|
||||
public class EmailDomainBlacklistRepository : BaseRepository<EmailDomainBlacklist, string>,
|
||||
IEmailDomainBlacklistRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EmailDomainBlacklistRepository" /> class.
|
||||
/// </summary>
|
||||
/// <param name="db">Instance of <see cref="IDbConnectionProvider" />.</param>
|
||||
public EmailDomainBlacklistRepository(IDbConnectionProvider db)
|
||||
: base(db)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEmailDomainBlacklistRepository" />
|
||||
public async Task<long> CountEntries()
|
||||
{
|
||||
return await WithDatabaseAsync(async connection =>
|
||||
await connection.QueryFirstAsync<long>($"SELECT COUNT(*) FROM {Table};"));
|
||||
}
|
||||
}
|
24
Astral.DAL/Repositories/UserGroupRepository.cs
Normal file
24
Astral.DAL/Repositories/UserGroupRepository.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
// <copyright file="UserGroupRepository.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Entities;
|
||||
using Astral.Core.Infrastructure;
|
||||
using Astral.Core.RepositoryInterfaces;
|
||||
using Injectio.Attributes;
|
||||
|
||||
namespace Astral.DAL.Repositories;
|
||||
|
||||
/// <inheritdoc cref="IUserGroupRepository" />
|
||||
[RegisterScoped]
|
||||
public class UserGroupRepository : BaseRepository<UserGroup, Guid>, IUserGroupRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserGroupRepository" /> class.
|
||||
/// </summary>
|
||||
/// <param name="db">Instance of <see cref="IDbConnectionProvider" />.</param>
|
||||
public UserGroupRepository(IDbConnectionProvider db)
|
||||
: base(db)
|
||||
{
|
||||
}
|
||||
}
|
24
Astral.DAL/Repositories/UserProfileRepository.cs
Normal file
24
Astral.DAL/Repositories/UserProfileRepository.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
// <copyright file="UserProfileRepository.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Entities;
|
||||
using Astral.Core.Infrastructure;
|
||||
using Astral.Core.RepositoryInterfaces;
|
||||
using Injectio.Attributes;
|
||||
|
||||
namespace Astral.DAL.Repositories;
|
||||
|
||||
/// <inheritdoc cref="IUserProfileRepository" />
|
||||
[RegisterScoped]
|
||||
public class UserProfileRepository : BaseRepository<UserProfile, Guid>, IUserProfileRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserProfileRepository" /> class.
|
||||
/// </summary>
|
||||
/// <param name="db">Instance of <see cref="IDbConnectionProvider" />.</param>
|
||||
public UserProfileRepository(IDbConnectionProvider db)
|
||||
: base(db)
|
||||
{
|
||||
}
|
||||
}
|
52
Astral.DAL/Repositories/UserRepository.cs
Normal file
52
Astral.DAL/Repositories/UserRepository.cs
Normal file
|
@ -0,0 +1,52 @@
|
|||
// <copyright file="UserRepository.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Entities;
|
||||
using Astral.Core.Infrastructure;
|
||||
using Astral.Core.RepositoryInterfaces;
|
||||
using Dapper;
|
||||
using Injectio.Attributes;
|
||||
|
||||
namespace Astral.DAL.Repositories;
|
||||
|
||||
/// <inheritdoc cref="IUserRepository" />
|
||||
[RegisterScoped]
|
||||
public class UserRepository : BaseRepository<User, Guid>, IUserRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserRepository" /> class.
|
||||
/// </summary>
|
||||
/// <param name="db">Instance of <see cref="IDbConnectionProvider" />.</param>
|
||||
public UserRepository(IDbConnectionProvider db)
|
||||
: base(db)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IUserRepository" />
|
||||
public async Task<long> CountUsersAsync()
|
||||
{
|
||||
return await WithDatabaseAsync(async connection =>
|
||||
await connection.QueryFirstAsync<long>($"SELECT COUNT(*) FROM {Table};"));
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IUserRepository" />
|
||||
public async Task<User> FindByUsername(string username)
|
||||
{
|
||||
return await WithDatabaseAsync(async connection =>
|
||||
await connection.QueryFirstOrDefaultAsync<User>($"SELECT * FROM {Table} WHERE username = @pUsername", new
|
||||
{
|
||||
pUsername = username
|
||||
}));
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IUserRepository" />
|
||||
public async Task<User> FindByEmailAddress(string emailAddress)
|
||||
{
|
||||
return await WithDatabaseAsync(async connection =>
|
||||
await connection.QueryFirstOrDefaultAsync<User>($"SELECT * FROM {Table} WHERE email = @pEmailAddress", new
|
||||
{
|
||||
pEmailAddress = emailAddress
|
||||
}));
|
||||
}
|
||||
}
|
25
Astral.Services/Astral.Services.csproj
Normal file
25
Astral.Services/Astral.Services.csproj
Normal file
|
@ -0,0 +1,25 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<CodeAnalysisRuleSet>../Alveus.ruleset</CodeAnalysisRuleSet>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Astral.Core\Astral.Core.csproj"/>
|
||||
<ProjectReference Include="..\Astral.DAL\Astral.DAL.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11"/>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
41
Astral.Services/Constants/ServiceErrorCodes.cs
Normal file
41
Astral.Services/Constants/ServiceErrorCodes.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
// <copyright file="ServiceErrorCodes.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Services.Constants;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of standard error codes.
|
||||
/// </summary>
|
||||
public static class ServiceErrorCodes
|
||||
{
|
||||
/// <summary>
|
||||
/// Username is already taken.
|
||||
/// </summary>
|
||||
public const string UsernameTaken = "username-taken";
|
||||
|
||||
/// <summary>
|
||||
/// Email address already registered to a user.
|
||||
/// </summary>
|
||||
public const string EmailAlreadyRegistered = "email-already-registered";
|
||||
|
||||
/// <summary>
|
||||
/// Invalid login credentials were provided.
|
||||
/// </summary>
|
||||
public const string InvalidCredentials = "invalid-credentials";
|
||||
|
||||
/// <summary>
|
||||
/// This user is suspended from logging in.
|
||||
/// </summary>
|
||||
public const string SuspendedUser = "user-suspended";
|
||||
|
||||
/// <summary>
|
||||
/// This user is banned from logging in.
|
||||
/// </summary>
|
||||
public const string BannedUser = "user-banned";
|
||||
|
||||
/// <summary>
|
||||
/// This user's account is not yet activated.
|
||||
/// </summary>
|
||||
public const string PendingActivation = "pending-activation";
|
||||
}
|
21
Astral.Services/Dtos/AuthenticateUserDto.cs
Normal file
21
Astral.Services/Dtos/AuthenticateUserDto.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
// <copyright file="AuthenticateUserDto.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Services.Dtos;
|
||||
|
||||
/// <summary>
|
||||
/// Authentication (login) request dto.
|
||||
/// </summary>
|
||||
public class AuthenticateUserDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Agent's username.
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Agent's password.
|
||||
/// </summary>
|
||||
public string Password { get; set; }
|
||||
}
|
38
Astral.Services/Dtos/CreateUserDto.cs
Normal file
38
Astral.Services/Dtos/CreateUserDto.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
// <copyright file="CreateUserDto.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Net;
|
||||
|
||||
namespace Astral.Services.Dtos;
|
||||
|
||||
/// <summary>
|
||||
/// Perform the creation of a new user.
|
||||
/// </summary>
|
||||
public class CreateUserDto
|
||||
{
|
||||
/// <summary>
|
||||
/// New user's username. Must be unique.
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// New user's email address. Must be unique.
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// New user's password. Must comply with password policy.
|
||||
/// </summary>
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// New user's IP address upon registration.
|
||||
/// </summary>
|
||||
public IPAddress IpAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If activation should be performed immediately.
|
||||
/// </summary>
|
||||
public bool ActivateImmediately { get; set; }
|
||||
}
|
61
Astral.Services/Dtos/UserDto.cs
Normal file
61
Astral.Services/Dtos/UserDto.cs
Normal file
|
@ -0,0 +1,61 @@
|
|||
// <copyright file="UserDto.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Services.Dtos;
|
||||
|
||||
/// <summary>
|
||||
/// Data transfer object for user.
|
||||
/// </summary>
|
||||
public class UserDto
|
||||
{
|
||||
/// <summary>
|
||||
/// User id.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the user was registered.
|
||||
/// </summary>
|
||||
public DateTime RegistrationDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the user was registered in ticks.
|
||||
/// </summary>
|
||||
public long RegistrationDateTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's name.
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's email address.
|
||||
/// </summary>
|
||||
public string EmailAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ip address of the creator.
|
||||
/// </summary>
|
||||
public string CreatorIp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the user last logged in.
|
||||
/// </summary>
|
||||
public DateTime LastLoggedIn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's role.
|
||||
/// </summary>
|
||||
public string UserRole { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Group for this user's connections.
|
||||
/// </summary>
|
||||
public Guid ConnectionGroup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Group for this user's friends.
|
||||
/// </summary>
|
||||
public Guid FriendsGroup { get; set; }
|
||||
}
|
41
Astral.Services/Dtos/UserGroupDto.cs
Normal file
41
Astral.Services/Dtos/UserGroupDto.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
// <copyright file="UserGroupDto.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Services.Dtos;
|
||||
|
||||
/// <summary>
|
||||
/// Data transfer object for user group.
|
||||
/// </summary>
|
||||
public class UserGroupDto
|
||||
{
|
||||
/// <summary>
|
||||
/// User group id.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the user group was created.
|
||||
/// </summary>
|
||||
public DateTime CreationDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the user group was created in ticks.
|
||||
/// </summary>
|
||||
public long CreationDateTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The title of the user group.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The description of the user group.
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is an internal-use user group.
|
||||
/// </summary>
|
||||
public bool Internal { get; set; }
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// <copyright file="EmailAlreadyRegisteredException.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Net;
|
||||
using Astral.Core.Exceptions;
|
||||
using Astral.Services.Constants;
|
||||
|
||||
namespace Astral.Services.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when an attempt to create a user with an email address already registered to an account.
|
||||
/// </summary>
|
||||
public class EmailAlreadyRegisteredException : ServiceException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EmailAlreadyRegisteredException" /> class.
|
||||
/// </summary>
|
||||
public EmailAlreadyRegisteredException()
|
||||
: base(
|
||||
ServiceErrorCodes.EmailAlreadyRegistered,
|
||||
"This email address is already registered to an account",
|
||||
HttpStatusCode.Conflict)
|
||||
{
|
||||
}
|
||||
}
|
26
Astral.Services/Exceptions/InvalidCredentialsException.cs
Normal file
26
Astral.Services/Exceptions/InvalidCredentialsException.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
// <copyright file="InvalidCredentialsException.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Net;
|
||||
using Astral.Core.Exceptions;
|
||||
using Astral.Services.Constants;
|
||||
|
||||
namespace Astral.Services.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown if username/password is incorrect.
|
||||
/// </summary>
|
||||
public class InvalidCredentialsException : ServiceException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvalidCredentialsException" /> class.
|
||||
/// </summary>
|
||||
public InvalidCredentialsException()
|
||||
: base(
|
||||
ServiceErrorCodes.InvalidCredentials,
|
||||
"The credentials provided were incorrect",
|
||||
HttpStatusCode.Unauthorized)
|
||||
{
|
||||
}
|
||||
}
|
23
Astral.Services/Exceptions/UnauthorizedException.cs
Normal file
23
Astral.Services/Exceptions/UnauthorizedException.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
// <copyright file="UnauthorizedException.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Net;
|
||||
using Astral.Core.Constants;
|
||||
using Astral.Core.Exceptions;
|
||||
|
||||
namespace Astral.Services.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when an attempt to perform an unauthorised action occurs.
|
||||
/// </summary>
|
||||
public class UnauthorizedException : ServiceException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UnauthorizedException" /> class.
|
||||
/// </summary>
|
||||
public UnauthorizedException()
|
||||
: base(CoreErrorCodes.Unauthorized, "You are not authorized to do that", HttpStatusCode.Unauthorized)
|
||||
{
|
||||
}
|
||||
}
|
23
Astral.Services/Exceptions/UserBannedException.cs
Normal file
23
Astral.Services/Exceptions/UserBannedException.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
// <copyright file="UserBannedException.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Net;
|
||||
using Astral.Core.Exceptions;
|
||||
using Astral.Services.Constants;
|
||||
|
||||
namespace Astral.Services.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when the logging in user is suspended.
|
||||
/// </summary>
|
||||
public class UserBannedException : ServiceException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserBannedException" /> class.
|
||||
/// </summary>
|
||||
public UserBannedException()
|
||||
: base(ServiceErrorCodes.BannedUser, "You are banned from logging in", HttpStatusCode.Unauthorized)
|
||||
{
|
||||
}
|
||||
}
|
26
Astral.Services/Exceptions/UserNotActivatedException.cs
Normal file
26
Astral.Services/Exceptions/UserNotActivatedException.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
// <copyright file="UserNotActivatedException.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Net;
|
||||
using Astral.Core.Exceptions;
|
||||
using Astral.Services.Constants;
|
||||
|
||||
namespace Astral.Services.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when the logging in user is suspended.
|
||||
/// </summary>
|
||||
public class UserNotActivatedException : ServiceException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserNotActivatedException" /> class.
|
||||
/// </summary>
|
||||
public UserNotActivatedException()
|
||||
: base(
|
||||
ServiceErrorCodes.PendingActivation,
|
||||
"Your account is pending activation. Please check your emails",
|
||||
HttpStatusCode.Unauthorized)
|
||||
{
|
||||
}
|
||||
}
|
23
Astral.Services/Exceptions/UserNotFoundException.cs
Normal file
23
Astral.Services/Exceptions/UserNotFoundException.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
// <copyright file="UserNotFoundException.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Net;
|
||||
using Astral.Core.Constants;
|
||||
using Astral.Core.Exceptions;
|
||||
|
||||
namespace Astral.Services.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when the logging in user is suspended.
|
||||
/// </summary>
|
||||
public class UserNotFoundException : ServiceException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserNotFoundException" /> class.
|
||||
/// </summary>
|
||||
public UserNotFoundException()
|
||||
: base(CoreErrorCodes.NotFoundError, "The provided user was not found", HttpStatusCode.NotFound)
|
||||
{
|
||||
}
|
||||
}
|
24
Astral.Services/Exceptions/UsernameTakenException.cs
Normal file
24
Astral.Services/Exceptions/UsernameTakenException.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
// <copyright file="UsernameTakenException.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Net;
|
||||
using Astral.Core.Exceptions;
|
||||
using Astral.Services.Constants;
|
||||
|
||||
namespace Astral.Services.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when there is an attempt to create a user with an already taken username.
|
||||
/// </summary>
|
||||
public class UsernameTakenException : ServiceException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UsernameTakenException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="username">The username attempted.</param>
|
||||
public UsernameTakenException(string username)
|
||||
: base(ServiceErrorCodes.UsernameTaken, $"Username {username} has already been taken", HttpStatusCode.Conflict)
|
||||
{
|
||||
}
|
||||
}
|
51
Astral.Services/Interfaces/ICryptographyService.cs
Normal file
51
Astral.Services/Interfaces/ICryptographyService.cs
Normal file
|
@ -0,0 +1,51 @@
|
|||
// <copyright file="ICryptographyService.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Cryptographic operations.
|
||||
/// </summary>
|
||||
public interface ICryptographyService
|
||||
{
|
||||
/// <summary>
|
||||
/// Generate a random salt.
|
||||
/// </summary>
|
||||
/// <param name="size">Size of the salt to generate - if omitted, will use PwdHashSettings:HashSize config.</param>
|
||||
/// <returns>Byte array containing the generated salt.</returns>
|
||||
byte[] GenerateSalt(int? size = null);
|
||||
|
||||
/// <summary>
|
||||
/// Hash a password string with the accompanying salt.
|
||||
/// </summary>
|
||||
/// <param name="password">The password to hash.</param>
|
||||
/// <param name="salt">The salt to use.</param>
|
||||
/// <returns>A byte array of PwdHashSettings:HashSize config size of the password.</returns>
|
||||
byte[] HashPassword(string password, byte[] salt);
|
||||
|
||||
/// <summary>
|
||||
/// Verify that the given password matches the password hash and salt.
|
||||
/// </summary>
|
||||
/// <param name="password">The password to test.</param>
|
||||
/// <param name="salt">The password's accompanying salt.</param>
|
||||
/// <param name="passwordHash">The password hash to test again.</param>
|
||||
/// <returns>True if verified, false otherwise.</returns>
|
||||
bool VerifyPassword(string password, byte[] salt, byte[] passwordHash);
|
||||
|
||||
/// <summary>
|
||||
/// Verify that the given password matches the password hash and salt.
|
||||
/// </summary>
|
||||
/// <param name="password">The password to test.</param>
|
||||
/// <param name="salt">The password's base64 encoded accompanying salt.</param>
|
||||
/// <param name="passwordHash">The password base64 hash to test again.</param>
|
||||
/// <returns>True if verified, false otherwise.</returns>
|
||||
bool VerifyPassword(string password, string salt, string passwordHash);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a cryptographically random string.
|
||||
/// </summary>
|
||||
/// <param name="length">Length of string to create.</param>
|
||||
/// <returns>A random string.</returns>
|
||||
string GenerateRandomString(int length);
|
||||
}
|
23
Astral.Services/Interfaces/IEmailDomainBlacklistService.cs
Normal file
23
Astral.Services/Interfaces/IEmailDomainBlacklistService.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
// <copyright file="IEmailDomainBlacklistService.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// A service providing a blacklist of email domains to prevent abuse.
|
||||
/// </summary>
|
||||
public interface IEmailDomainBlacklistService
|
||||
{
|
||||
/// <summary>
|
||||
/// Update the blacklist.
|
||||
/// </summary>
|
||||
Task UpdateBlacklist();
|
||||
|
||||
/// <summary>
|
||||
/// Check the email against the blacklist.
|
||||
/// </summary>
|
||||
/// <param name="emailAddress">The email address to test.</param>
|
||||
/// <returns>True if the email address' domain is in the blacklist.</returns>
|
||||
Task<bool> CheckBlacklist(string emailAddress);
|
||||
}
|
17
Astral.Services/Interfaces/IIdentityProvider.cs
Normal file
17
Astral.Services/Interfaces/IIdentityProvider.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
// <copyright file="IIdentityProvider.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Identify who the requesting user is.
|
||||
/// </summary>
|
||||
public interface IIdentityProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the user id of the requester.
|
||||
/// </summary>
|
||||
/// <returns>String representing the user id, or null if none.</returns>
|
||||
string GetRequestingUserId();
|
||||
}
|
16
Astral.Services/Interfaces/IInitialUserService.cs
Normal file
16
Astral.Services/Interfaces/IInitialUserService.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
// <copyright file="IInitialUserService.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Service responsible for creating the initial user if no other users exist.
|
||||
/// </summary>
|
||||
public interface IInitialUserService
|
||||
{
|
||||
/// <summary>
|
||||
/// If an initial user is required, then create it.
|
||||
/// </summary>
|
||||
Task CreateFirstUserIfRequiredAsync();
|
||||
}
|
32
Astral.Services/Interfaces/IUserGroupService.cs
Normal file
32
Astral.Services/Interfaces/IUserGroupService.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
// <copyright file="IUserGroupService.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Entities;
|
||||
using Astral.Services.Dtos;
|
||||
|
||||
namespace Astral.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="UserGroup" /> service.
|
||||
/// </summary>
|
||||
public interface IUserGroupService
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new user group.
|
||||
/// </summary>
|
||||
/// <param name="ownerUserId">The id of the owner <see cref="User" />.</param>
|
||||
/// <param name="title">The title of the new group.</param>
|
||||
/// <param name="description">The description of the new group.</param>
|
||||
/// <returns>The new user group dto.</returns>
|
||||
Task<UserGroupDto> CreateUserGroup(Guid ownerUserId, string title, string description);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new internal user group.
|
||||
/// </summary>
|
||||
/// <param name="ownerUserId">The id of the owner <see cref="User" />.</param>
|
||||
/// <param name="title">The title of the new group.</param>
|
||||
/// <param name="description">The description of the new group.</param>
|
||||
/// <returns>The new user group dto.</returns>
|
||||
Task<UserGroupDto> CreateInternalGroup(Guid ownerUserId, string title, string description);
|
||||
}
|
27
Astral.Services/Interfaces/IUserService.cs
Normal file
27
Astral.Services/Interfaces/IUserService.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
// <copyright file="IUserService.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Services.Dtos;
|
||||
|
||||
namespace Astral.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// User Service.
|
||||
/// </summary>
|
||||
public interface IUserService
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new user and return its user dto.
|
||||
/// </summary>
|
||||
/// <param name="createUser">Instance of <see cref="CreateUserDto" />.</param>
|
||||
/// <returns>Instance of <see cref="UserDto" />.</returns>
|
||||
Task<UserDto> CreateNewUser(CreateUserDto createUser);
|
||||
|
||||
/// <summary>
|
||||
/// Fetch a user based on their user id.
|
||||
/// </summary>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <returns>Instance of <see cref="UserDto" />, or null if not found.</returns>
|
||||
Task<UserDto> FindById(string userId);
|
||||
}
|
21
Astral.Services/Options/EmailDomainBlacklistOptions.cs
Normal file
21
Astral.Services/Options/EmailDomainBlacklistOptions.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
// <copyright file="EmailDomainBlacklistOptions.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Services.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the email domain blacklist service.
|
||||
/// </summary>
|
||||
public class EmailDomainBlacklistOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the email domain blacklist should be used.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Url of a master list for email address domains to blacklist.
|
||||
/// </summary>
|
||||
public string MasterList { get; set; }
|
||||
}
|
26
Astral.Services/Options/InitialUserOptions.cs
Normal file
26
Astral.Services/Options/InitialUserOptions.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
// <copyright file="InitialUserOptions.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Services.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the first user created if none other exist.
|
||||
/// </summary>
|
||||
public class InitialUserOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Username.
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Email address.
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Password.
|
||||
/// </summary>
|
||||
public string Password { get; set; }
|
||||
}
|
37
Astral.Services/Options/PwdHashOptions.cs
Normal file
37
Astral.Services/Options/PwdHashOptions.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
// <copyright file="PwdHashOptions.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Services.Options;
|
||||
|
||||
/// <summary>
|
||||
/// How the password should be hashed.
|
||||
/// Refer to https://argon2-cffi.readthedocs.io/en/stable/argon2.html.
|
||||
/// </summary>
|
||||
public class PwdHashOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of threads to use. (Higher the better).
|
||||
/// </summary>
|
||||
public int DegreeOfParallelism { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of iterations over the memory.
|
||||
/// </summary>
|
||||
public int NumberOfIterations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Memory used by the algorithm.
|
||||
/// </summary>
|
||||
public int MemoryToUseKb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Salt size in bytes.
|
||||
/// </summary>
|
||||
public int SaltSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash size in bytes.
|
||||
/// </summary>
|
||||
public int HashSize { get; set; }
|
||||
}
|
26
Astral.Services/Options/RegistrationOptions.cs
Normal file
26
Astral.Services/Options/RegistrationOptions.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
// <copyright file="RegistrationOptions.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Services.Options;
|
||||
|
||||
/// <summary>
|
||||
/// User registration configuration.
|
||||
/// </summary>
|
||||
public class RegistrationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether email verification should be required.
|
||||
/// </summary>
|
||||
public bool RequireEmailActivation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default profile hero image url.
|
||||
/// </summary>
|
||||
public string DefaultHeroImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default thumbnail image url.
|
||||
/// </summary>
|
||||
public string DefaultThumbnailImageUrl { get; set; }
|
||||
}
|
35
Astral.Services/Profiles/AutomapperProfile.cs
Normal file
35
Astral.Services/Profiles/AutomapperProfile.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
// <copyright file="AutomapperProfile.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Entities;
|
||||
using Astral.Services.Dtos;
|
||||
using AutoMapper;
|
||||
|
||||
namespace Astral.Services.Profiles;
|
||||
|
||||
/// <summary>
|
||||
/// Profile for AutoMapper.
|
||||
/// </summary>
|
||||
public class AutomapperProfile : Profile
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AutomapperProfile" /> class.
|
||||
/// </summary>
|
||||
public AutomapperProfile()
|
||||
{
|
||||
CreateMap<User, UserDto>()
|
||||
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
|
||||
.ForMember(dest => dest.RegistrationDate, opt => opt.MapFrom(src => src.CreatedAt))
|
||||
.ForMember(dest => dest.RegistrationDateTicks, opt => opt.MapFrom(src => src.CreatedAt.Ticks))
|
||||
.ForMember(dest => dest.UserRole, opt => opt.MapFrom(src => src.UserRole));
|
||||
|
||||
CreateMap<UserGroup, UserGroupDto>()
|
||||
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
|
||||
.ForMember(dest => dest.CreationDate, opt => opt.MapFrom(src => src.CreatedAt))
|
||||
.ForMember(dest => dest.CreationDateTicks, opt => opt.MapFrom(src => src.CreatedAt.Ticks))
|
||||
.ForMember(dest => dest.Title, opt => opt.MapFrom(src => src.Title))
|
||||
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
|
||||
.ForMember(dest => dest.Internal, opt => opt.MapFrom(src => src.Internal));
|
||||
}
|
||||
}
|
70
Astral.Services/Services/CryptographyService.cs
Normal file
70
Astral.Services/Services/CryptographyService.cs
Normal file
|
@ -0,0 +1,70 @@
|
|||
// <copyright file="CryptographyService.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Astral.Services.Interfaces;
|
||||
using Astral.Services.Options;
|
||||
using Injectio.Attributes;
|
||||
using Konscious.Security.Cryptography;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Astral.Services.Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
[RegisterSingleton]
|
||||
public class CryptographyService : ICryptographyService
|
||||
{
|
||||
private readonly PwdHashOptions _configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CryptographyService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="pwdHashSettings">Instance of <see cref="IOptions{PwdHashOptions}" />.</param>
|
||||
public CryptographyService(IOptions<PwdHashOptions> pwdHashSettings)
|
||||
{
|
||||
_configuration = pwdHashSettings.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] GenerateSalt(int? size = null)
|
||||
{
|
||||
var result = RandomNumberGenerator.GetBytes(size ?? _configuration.SaltSize);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] HashPassword(string password, byte[] salt)
|
||||
{
|
||||
var argon2Id = new Argon2id(Encoding.UTF8.GetBytes(password));
|
||||
argon2Id.Salt = salt;
|
||||
argon2Id.DegreeOfParallelism = _configuration.DegreeOfParallelism;
|
||||
argon2Id.Iterations = _configuration.NumberOfIterations;
|
||||
argon2Id.MemorySize = _configuration.MemoryToUseKb;
|
||||
var bytes = argon2Id.GetBytes(_configuration.HashSize);
|
||||
GC.Collect();
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool VerifyPassword(string password, byte[] salt, byte[] passwordHash)
|
||||
{
|
||||
var checkHash = HashPassword(password, salt);
|
||||
return passwordHash.SequenceEqual(checkHash);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool VerifyPassword(string password, string salt, string passwordHash)
|
||||
{
|
||||
var checkHash = HashPassword(password, Convert.FromBase64String(salt));
|
||||
return Convert.FromBase64String(passwordHash).SequenceEqual(checkHash);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GenerateRandomString(int length)
|
||||
{
|
||||
const string availableChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
||||
return RandomNumberGenerator.GetString(availableChars, length);
|
||||
}
|
||||
}
|
128
Astral.Services/Services/EmailDomainBlacklistService.cs
Normal file
128
Astral.Services/Services/EmailDomainBlacklistService.cs
Normal file
|
@ -0,0 +1,128 @@
|
|||
// <copyright file="EmailDomainBlacklistService.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Net.Mail;
|
||||
using Astral.Core.Entities;
|
||||
using Astral.Core.Extensions;
|
||||
using Astral.Core.RepositoryInterfaces;
|
||||
using Astral.Services.Interfaces;
|
||||
using Astral.Services.Options;
|
||||
using Injectio.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Astral.Services.Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
[RegisterScoped]
|
||||
public class EmailDomainBlacklistService : IEmailDomainBlacklistService
|
||||
{
|
||||
private readonly EmailDomainBlacklistOptions _configuration;
|
||||
private readonly ILogger<EmailDomainBlacklistService> _logger;
|
||||
private readonly IEmailDomainBlacklistRepository _repository;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EmailDomainBlacklistService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">Instance of <see cref="IOptions{TOptions}" />.</param>
|
||||
/// <param name="repository">Instance of <see cref="EmailDomainBlacklistOptions" />.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger" />.</param>
|
||||
public EmailDomainBlacklistService(
|
||||
IOptions<EmailDomainBlacklistOptions> configuration,
|
||||
IEmailDomainBlacklistRepository repository,
|
||||
ILogger<EmailDomainBlacklistService> logger)
|
||||
{
|
||||
_configuration = configuration.Value;
|
||||
_repository = repository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UpdateBlacklist()
|
||||
{
|
||||
if (!_configuration.Enabled)
|
||||
{
|
||||
_logger.LogInformation("Email address domain blacklist disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Updating email address domain blacklist");
|
||||
|
||||
try
|
||||
{
|
||||
var client = new HttpClient();
|
||||
|
||||
// Attempt to fetch master list.
|
||||
var result = await client.GetAsync(_configuration.MasterList);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Failed to retrieve up-to-date blacklisted domains. Http status code: {code}",
|
||||
result.StatusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
var content = await result.Content.ReadAsStringAsync();
|
||||
var entries = content.Split(
|
||||
["\r\n", "\r", "\n"],
|
||||
StringSplitOptions.None)
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s) && s.Trim()[..1] != "#" && s.Trim()[..1] != "[" &&
|
||||
s.Trim()[..1] != "!");
|
||||
|
||||
var existing = await _repository.GetAllAsync();
|
||||
|
||||
var newDomains = entries.Except(existing.Select(e => e.Domain)).ToList();
|
||||
|
||||
_logger.LogInformation("Adding {count} new domains to the blacklist", newDomains.Count);
|
||||
|
||||
// Add new domains to blacklist
|
||||
var listChunks = newDomains.ChunkBy(1000);
|
||||
newDomains.Clear();
|
||||
|
||||
foreach (var chunk in listChunks)
|
||||
{
|
||||
await _repository.AddAsync(chunk.Select(d => new EmailDomainBlacklist
|
||||
{
|
||||
Domain = d,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
}));
|
||||
}
|
||||
|
||||
GC.Collect(2, GCCollectionMode.Aggressive);
|
||||
GC.WaitForFullGCComplete();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError("Failed to retrieve up-to-date blacklisted domains: {exception}", exception);
|
||||
}
|
||||
|
||||
var count = await _repository.CountEntries();
|
||||
_logger.LogInformation("Total email address domains in blacklist: {count}", count);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> CheckBlacklist(string emailAddress)
|
||||
{
|
||||
if (!_configuration.Enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var mailAddress = new MailAddress(emailAddress.ToLowerInvariant());
|
||||
var domain = mailAddress.Host;
|
||||
var blacklisted = await _repository.FindByIdAsync(domain);
|
||||
return blacklisted is not null;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Failed to determine if following email address is in the blacklist: {email}. {exception}",
|
||||
emailAddress.MaskEmailAddress(),
|
||||
exception);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
78
Astral.Services/Services/InitialUserService.cs
Normal file
78
Astral.Services/Services/InitialUserService.cs
Normal file
|
@ -0,0 +1,78 @@
|
|||
// <copyright file="InitialUserService.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using System.Net;
|
||||
using Astral.Core.Constants;
|
||||
using Astral.Core.RepositoryInterfaces;
|
||||
using Astral.Services.Dtos;
|
||||
using Astral.Services.Interfaces;
|
||||
using Astral.Services.Options;
|
||||
using Injectio.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Astral.Services.Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
[RegisterScoped]
|
||||
public class InitialUserService : IInitialUserService
|
||||
{
|
||||
private readonly InitialUserOptions _configuration;
|
||||
private readonly ILogger<InitialUserService> _logger;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InitialUserService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="initialUserConfiguration">Instance of <see cref="IOptions{InitialUserOptions}" />.</param>
|
||||
/// <param name="userService">Instance of <see cref="IUserService" />.</param>
|
||||
/// <param name="userRepository">Instance of <see cref="IUserRepository" />.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger" />.</param>
|
||||
public InitialUserService(
|
||||
IOptions<InitialUserOptions> initialUserConfiguration,
|
||||
IUserService userService,
|
||||
IUserRepository userRepository,
|
||||
ILogger<InitialUserService> logger)
|
||||
{
|
||||
_configuration = initialUserConfiguration.Value;
|
||||
_userService = userService;
|
||||
_userRepository = userRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task CreateFirstUserIfRequiredAsync()
|
||||
{
|
||||
// Check if any users exist.
|
||||
var userCount = await _userRepository.CountUsersAsync();
|
||||
|
||||
if (userCount > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Creating initial user. Username: {username}", _configuration.Username);
|
||||
_logger.LogWarning("*************************************************************");
|
||||
_logger.LogWarning("* CHANGE INITIAL USER PASSWORD IMMEDIATELY AFTER LOGGING IN *");
|
||||
_logger.LogWarning("*************************************************************");
|
||||
|
||||
var newUser = await _userService.CreateNewUser(new CreateUserDto
|
||||
{
|
||||
Username = _configuration.Username,
|
||||
Email = _configuration.Email,
|
||||
Password = _configuration.Password,
|
||||
ActivateImmediately = true,
|
||||
IpAddress = IPAddress.Any
|
||||
});
|
||||
|
||||
// Promote user to admin.
|
||||
var userEntity = await _userRepository.FindByIdAsync(newUser.Id);
|
||||
userEntity.UserRole = UserRole.Admin;
|
||||
|
||||
await _userRepository.UpdateAsync(userEntity);
|
||||
|
||||
_logger.LogInformation("Initial user {username} created", newUser.Id);
|
||||
}
|
||||
}
|
137
Astral.Services/Services/UserGroupService.cs
Normal file
137
Astral.Services/Services/UserGroupService.cs
Normal file
|
@ -0,0 +1,137 @@
|
|||
// <copyright file="UserGroupService.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Entities;
|
||||
using Astral.Core.Exceptions;
|
||||
using Astral.Core.RepositoryInterfaces;
|
||||
using Astral.Services.Dtos;
|
||||
using Astral.Services.Exceptions;
|
||||
using Astral.Services.Interfaces;
|
||||
using AutoMapper;
|
||||
using Injectio.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Astral.Services.Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
[RegisterScoped]
|
||||
public class UserGroupService : IUserGroupService
|
||||
{
|
||||
private readonly ILogger<UserGroupService> _logger;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IUserGroupRepository _userGroupRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserGroupService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="userGroupRepository">Instance of <see cref="IUserGroupRepository" />.</param>
|
||||
/// <param name="userRepository">Instance of <see cref="IUserRepository" />.</param>
|
||||
/// <param name="mapper">Instance of <see cref="IMapper" />.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger" />.</param>
|
||||
public UserGroupService(
|
||||
IUserGroupRepository userGroupRepository,
|
||||
IUserRepository userRepository,
|
||||
IMapper mapper,
|
||||
ILogger<UserGroupService> logger)
|
||||
{
|
||||
_userGroupRepository = userGroupRepository;
|
||||
_userRepository = userRepository;
|
||||
_mapper = mapper;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UserGroupDto> CreateUserGroup(Guid ownerUserId, string title, string description)
|
||||
{
|
||||
var user = await _userRepository.FindByIdAsync(ownerUserId);
|
||||
if (user is null)
|
||||
{
|
||||
_logger.LogError("Attempted to create a user group for a user that does not exist: {userId}", ownerUserId);
|
||||
throw new UserNotFoundException();
|
||||
}
|
||||
|
||||
var uniqueId = await GetUniqueId();
|
||||
|
||||
var userGroup = new UserGroup
|
||||
{
|
||||
Id = uniqueId,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
OwnerUserId = ownerUserId,
|
||||
Title = title,
|
||||
Description = description ?? string.Empty,
|
||||
Internal = false
|
||||
};
|
||||
|
||||
_logger.LogInformation(
|
||||
"Creating new user group {groupTitle} [{groupId}] for user {user} [{userId}]",
|
||||
userGroup.Title,
|
||||
userGroup.Id,
|
||||
user.Username,
|
||||
user.Id);
|
||||
|
||||
await _userGroupRepository.AddAsync(userGroup);
|
||||
|
||||
return _mapper.Map<UserGroupDto>(userGroup);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UserGroupDto> CreateInternalGroup(Guid ownerUserId, string title, string description)
|
||||
{
|
||||
var user = await _userRepository.FindByIdAsync(ownerUserId);
|
||||
if (user is null)
|
||||
{
|
||||
_logger.LogError("Attempted to create a user group for a user that does not exist: {userId}", ownerUserId);
|
||||
throw new UserNotFoundException();
|
||||
}
|
||||
|
||||
var uniqueId = await GetUniqueId();
|
||||
|
||||
var userGroup = new UserGroup
|
||||
{
|
||||
Id = uniqueId,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
OwnerUserId = ownerUserId,
|
||||
Title = title,
|
||||
Description = description ?? string.Empty,
|
||||
Internal = true
|
||||
};
|
||||
|
||||
_logger.LogInformation(
|
||||
"Creating new internal user group {groupTitle} [{groupId}] for user {user} [{userId}]",
|
||||
userGroup.Title,
|
||||
userGroup.Id,
|
||||
user.Username,
|
||||
user.Id);
|
||||
|
||||
await _userGroupRepository.AddAsync(userGroup);
|
||||
|
||||
return _mapper.Map<UserGroupDto>(userGroup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a unique guid.
|
||||
/// </summary>
|
||||
/// <returns>The new guid.</returns>
|
||||
private async Task<Guid> GetUniqueId()
|
||||
{
|
||||
var attempt = 0;
|
||||
const int maxAttempts = 10;
|
||||
|
||||
var newId = Guid.NewGuid();
|
||||
while (await _userGroupRepository.FindByIdAsync(newId) is not null)
|
||||
{
|
||||
attempt++;
|
||||
if (attempt >= maxAttempts)
|
||||
{
|
||||
_logger.LogCritical("Unable to generate a unique guid for user group");
|
||||
throw new UnexpectedErrorException();
|
||||
}
|
||||
|
||||
newId = Guid.NewGuid();
|
||||
}
|
||||
|
||||
return newId;
|
||||
}
|
||||
}
|
193
Astral.Services/Services/UserService.cs
Normal file
193
Astral.Services/Services/UserService.cs
Normal file
|
@ -0,0 +1,193 @@
|
|||
// <copyright file="UserService.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Constants;
|
||||
using Astral.Core.Entities;
|
||||
using Astral.Core.Exceptions;
|
||||
using Astral.Core.Extensions;
|
||||
using Astral.Core.Infrastructure;
|
||||
using Astral.Core.RepositoryInterfaces;
|
||||
using Astral.Services.Dtos;
|
||||
using Astral.Services.Exceptions;
|
||||
using Astral.Services.Interfaces;
|
||||
using Astral.Services.Options;
|
||||
using Astral.Services.Validators;
|
||||
using AutoMapper;
|
||||
using FluentValidation;
|
||||
using Injectio.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Astral.Services.Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
[RegisterScoped]
|
||||
public class UserService : IUserService
|
||||
{
|
||||
private readonly IValidator<CreateUserDto> _createUserValidator;
|
||||
private readonly ICryptographyService _cryptographyService;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly RegistrationOptions _registrationConfiguration;
|
||||
private readonly ITransactionProvider _transactionProvider;
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserProfileRepository _userProfileRepository;
|
||||
private readonly ILogger<UserService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="userRepository">Instance of <see cref="IUserRepository" />.</param>
|
||||
/// <param name="cryptographyService">Instance of <see cref="ICryptographyService" />.</param>
|
||||
/// <param name="userGroupService">Instance of <see cref="IUserGroupService" />.</param>
|
||||
/// <param name="mapper">Instance of <see cref="IMapper" />.</param>
|
||||
/// <param name="createUserValidator">Instance of <see cref="CreateUserValidator" />.</param>
|
||||
/// <param name="registrationConfig">Instance of <see cref="IOptions{RegistrationOptions}" />.</param>
|
||||
/// <param name="transactionProvider">Instance of <see cref="ITransactionProvider" />.</param>
|
||||
/// <param name="transactionProvider">Instance of <see cref="ITransactionProvider" />.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger" />.</param>
|
||||
public UserService(
|
||||
IUserRepository userRepository,
|
||||
ICryptographyService cryptographyService,
|
||||
IUserGroupService userGroupService,
|
||||
IMapper mapper,
|
||||
IValidator<CreateUserDto> createUserValidator,
|
||||
IOptions<RegistrationOptions> registrationConfig,
|
||||
ITransactionProvider transactionProvider,
|
||||
IUserProfileRepository userProfileRepository,
|
||||
ILogger<UserService> logger)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_cryptographyService = cryptographyService;
|
||||
_userGroupService = userGroupService;
|
||||
_mapper = mapper;
|
||||
_createUserValidator = createUserValidator;
|
||||
_registrationConfiguration = registrationConfig.Value;
|
||||
_transactionProvider = transactionProvider;
|
||||
_userProfileRepository = userProfileRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UserDto> CreateNewUser(CreateUserDto createUser)
|
||||
{
|
||||
createUser.Username = createUser.Username.ToLower();
|
||||
createUser.Email = createUser.Email.ToLowerInvariant();
|
||||
|
||||
await _createUserValidator.ValidateAndThrowAsync(createUser);
|
||||
|
||||
// Check if username is already taken.
|
||||
var existingUser = await _userRepository.FindByUsername(createUser.Username);
|
||||
if (existingUser is not null)
|
||||
{
|
||||
throw new UsernameTakenException(createUser.Username);
|
||||
}
|
||||
|
||||
// Check if email already registered.
|
||||
existingUser = await _userRepository.FindByEmailAddress(createUser.Email);
|
||||
if (existingUser is not null)
|
||||
{
|
||||
throw new EmailAlreadyRegisteredException();
|
||||
}
|
||||
|
||||
var user = new User
|
||||
{
|
||||
Id = await GetUniqueId(),
|
||||
Username = createUser.Username,
|
||||
EmailAddress = createUser.Email,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatorIp = createUser.IpAddress.ToString(),
|
||||
UserRole = UserRole.User
|
||||
};
|
||||
|
||||
var salt = _cryptographyService.GenerateSalt();
|
||||
var hash = _cryptographyService.HashPassword(createUser.Password, salt);
|
||||
|
||||
user.PasswordHash = Convert.ToBase64String(hash);
|
||||
user.PasswordSalt = Convert.ToBase64String(salt);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Creating new user {username} ({email}) [{id}]",
|
||||
user.Username,
|
||||
user.EmailAddress.MaskEmailAddress(),
|
||||
user.Id);
|
||||
|
||||
if (!_registrationConfiguration.RequireEmailActivation || createUser.ActivateImmediately)
|
||||
{
|
||||
}
|
||||
|
||||
using var transaction = await _transactionProvider.BeginTransactionAsync();
|
||||
|
||||
await _userRepository.AddAsync(user);
|
||||
|
||||
// Setup internal user groups.
|
||||
var connectionGroup = await _userGroupService.CreateInternalGroup(
|
||||
user.Id,
|
||||
$"User {user.Id} connections",
|
||||
$"Connections for {user.Username}");
|
||||
|
||||
var friendsGroup = await _userGroupService.CreateInternalGroup(
|
||||
user.Id,
|
||||
$"User {user.Id} friends",
|
||||
$"Friends for {user.Username}");
|
||||
|
||||
user.ConnectionGroup = connectionGroup.Id;
|
||||
user.FriendsGroup = friendsGroup.Id;
|
||||
|
||||
// Setup user profile.
|
||||
var profile = new UserProfile()
|
||||
{
|
||||
Id = user.Id,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
HeroImageUrl = _registrationConfiguration.DefaultHeroImageUrl,
|
||||
ThumbnailImageUrl = _registrationConfiguration.DefaultThumbnailImageUrl
|
||||
};
|
||||
|
||||
await _userProfileRepository.AddAsync(profile);
|
||||
|
||||
await _userRepository.UpdateAsync(user);
|
||||
|
||||
transaction.Commit();
|
||||
|
||||
return _mapper.Map<UserDto>(user);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UserDto> FindById(string userId)
|
||||
{
|
||||
if (!Guid.TryParse(userId, out var userGuid))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var user = await _userRepository.FindByIdAsync(userGuid);
|
||||
|
||||
return _mapper.Map<UserDto>(user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a unique guid.
|
||||
/// </summary>
|
||||
/// <returns>The new guid.</returns>
|
||||
private async Task<Guid> GetUniqueId()
|
||||
{
|
||||
var attempt = 0;
|
||||
const int maxAttempts = 10;
|
||||
|
||||
var newId = Guid.NewGuid();
|
||||
while (await _userRepository.FindByIdAsync(newId) is not null)
|
||||
{
|
||||
attempt++;
|
||||
if (attempt >= maxAttempts)
|
||||
{
|
||||
_logger.LogCritical("Unable to generate a unique guid for user group");
|
||||
throw new UnexpectedErrorException();
|
||||
}
|
||||
|
||||
newId = Guid.NewGuid();
|
||||
}
|
||||
|
||||
return newId;
|
||||
}
|
||||
}
|
32
Astral.Services/Validators/AuthenticateUserValidator.cs
Normal file
32
Astral.Services/Validators/AuthenticateUserValidator.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
// <copyright file="AuthenticateUserValidator.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Services.Dtos;
|
||||
using FluentValidation;
|
||||
using Injectio.Attributes;
|
||||
|
||||
namespace Astral.Services.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validation for <see cref="AuthenticateUserDto" />.
|
||||
/// </summary>
|
||||
[RegisterScoped]
|
||||
public class AuthenticateUserValidator : AbstractValidator<AuthenticateUserDto>
|
||||
{
|
||||
private const int MinimumUsernameLength = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthenticateUserValidator" /> class.
|
||||
/// </summary>
|
||||
public AuthenticateUserValidator()
|
||||
{
|
||||
RuleFor(dto => dto.Username).NotEmpty().WithMessage("Username must be provided")
|
||||
.MinimumLength(MinimumUsernameLength)
|
||||
.WithMessage("Username must be a minimum of " + MinimumUsernameLength + " characters")
|
||||
.Matches(@"^[A-Za-z0-9._-]+$")
|
||||
.WithMessage("Username can only contain letters, numbers, hyphens, dashes and periods.");
|
||||
|
||||
RuleFor(dto => dto.Password).NotEmpty().WithMessage("Password must be provided");
|
||||
}
|
||||
}
|
44
Astral.Services/Validators/CreateUserValidator.cs
Normal file
44
Astral.Services/Validators/CreateUserValidator.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
// <copyright file="CreateUserValidator.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Services.Dtos;
|
||||
using Astral.Services.Interfaces;
|
||||
using FluentValidation;
|
||||
using Injectio.Attributes;
|
||||
|
||||
namespace Astral.Services.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validation for <see cref="CreateUserDto" />.
|
||||
/// </summary>
|
||||
[RegisterScoped]
|
||||
public class CreateUserValidator : AbstractValidator<CreateUserDto>
|
||||
{
|
||||
private const int MinimumUsernameLength = 4;
|
||||
private const int MaximumUsernameLength = 16;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CreateUserValidator" /> class.
|
||||
/// </summary>
|
||||
/// <param name="emailDomainBlacklistService">Instance of <see cref="IEmailDomainBlacklistService" />.</param>
|
||||
public CreateUserValidator(IEmailDomainBlacklistService emailDomainBlacklistService)
|
||||
{
|
||||
RuleFor(dto => dto.Username).NotEmpty().WithMessage("Username must be provided")
|
||||
.MinimumLength(MinimumUsernameLength)
|
||||
.WithMessage("Username must be a minimum of " + MinimumUsernameLength + " characters")
|
||||
.MaximumLength(MaximumUsernameLength)
|
||||
.WithMessage("Username must be within " + MaximumUsernameLength + " characters")
|
||||
.Matches(@"^[A-Za-z0-9._-]+$")
|
||||
.WithMessage("Username can only contain letters, numbers, hyphens, dashes and periods.");
|
||||
|
||||
RuleFor(dto => dto.Email).NotEmpty().WithMessage("Email address must be provided")
|
||||
.EmailAddress().WithMessage("Email address is not a valid email address");
|
||||
|
||||
RuleFor(dto => dto.Email).MustAsync(async (email, _) =>
|
||||
await emailDomainBlacklistService.CheckBlacklist(email) == false)
|
||||
.WithMessage("Email addresses from this domain cannot be used");
|
||||
|
||||
RuleFor(dto => dto.Password).NotEmpty().WithMessage("Password must be provided");
|
||||
}
|
||||
}
|
41
AstralApi.sln
Normal file
41
AstralApi.sln
Normal file
|
@ -0,0 +1,41 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Astral.ApiServer", "Astral.ApiServer\Astral.ApiServer.csproj", "{9A2C68B3-F38D-424E-AAB9-86BB5F4467CA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Astral.Core", "Astral.Core\Astral.Core.csproj", "{31AF880C-0699-42B2-A5D0-896D1A9EA8DF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Astral.DAL", "Astral.DAL\Astral.DAL.csproj", "{AD0427F5-4B88-4FE5-9F1C-28184482B551}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Astral.Services", "Astral.Services\Astral.Services.csproj", "{7594740E-7061-46C9-A870-7E3687D3BC36}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution", "Solution", "{EB52D62B-1EB7-45C0-AB8F-71C3685F9240}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
Alveus.ruleset = Alveus.ruleset
|
||||
LICENSE = LICENSE
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{9A2C68B3-F38D-424E-AAB9-86BB5F4467CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9A2C68B3-F38D-424E-AAB9-86BB5F4467CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9A2C68B3-F38D-424E-AAB9-86BB5F4467CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9A2C68B3-F38D-424E-AAB9-86BB5F4467CA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{31AF880C-0699-42B2-A5D0-896D1A9EA8DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{31AF880C-0699-42B2-A5D0-896D1A9EA8DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{31AF880C-0699-42B2-A5D0-896D1A9EA8DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{31AF880C-0699-42B2-A5D0-896D1A9EA8DF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AD0427F5-4B88-4FE5-9F1C-28184482B551}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AD0427F5-4B88-4FE5-9F1C-28184482B551}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AD0427F5-4B88-4FE5-9F1C-28184482B551}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AD0427F5-4B88-4FE5-9F1C-28184482B551}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7594740E-7061-46C9-A870-7E3687D3BC36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7594740E-7061-46C9-A870-7E3687D3BC36}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7594740E-7061-46C9-A870-7E3687D3BC36}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7594740E-7061-46C9-A870-7E3687D3BC36}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 alveus.dev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
11
README.md
Normal file
11
README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Astral API Service
|
||||
|
||||
Astral api directory service providing directory services.
|
||||
|
||||
Designed for Overte interface and domain servers.
|
||||
|
||||
## Note
|
||||
|
||||
There will be no compatibility offered for the Overte Metaverse Dashboard.
|
||||
|
||||
More information to come.
|
Loading…
Reference in a new issue