Initial commit
This commit is contained in:
commit
f1e46d9d6f
25
.dockerignore
Normal file
25
.dockerignore
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/.idea
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/azds.yaml
|
||||||
|
**/bin
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/obj
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
LICENSE
|
||||||
|
README.md
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
/packages/
|
||||||
|
riderModule.iml
|
||||||
|
/_ReSharper.Caches/
|
||||||
|
.idea
|
||||||
|
*.DotSettings.user
|
26
BrunoColllection/Galaeth/Authentication/Change Password.bru
Normal file
26
BrunoColllection/Galaeth/Authentication/Change Password.bru
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
meta {
|
||||||
|
name: Change Password
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{Host}}/v1/auth/pwd
|
||||||
|
body: json
|
||||||
|
auth: bearer
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
:
|
||||||
|
}
|
||||||
|
|
||||||
|
auth:bearer {
|
||||||
|
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyLWlkIjoiYWQyZjI2MmUtOTVhMi00MDUzLTlmMjMtNmMxNjg5NWEwNGE3Iiwicm9sZSI6IlJvb3QiLCJuYmYiOjE3MzE3ODUyMTMsImV4cCI6MTczMTc4NTUxMywiaWF0IjoxNzMxNzg1MjEzLCJpc3MiOiJ5b3VyLWlzc3VlciIsImF1ZCI6InlvdXItYXVkaWVuY2UifQ.udVpVx3maT_Cn9WHkBOCukT_o1oGctqsXboYjL32I3Y
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"oldPassword": "{{Password}}",
|
||||||
|
"newPassword": "MyNewPassword123!"
|
||||||
|
}
|
||||||
|
}
|
23
BrunoColllection/Galaeth/Authentication/Login.bru
Normal file
23
BrunoColllection/Galaeth/Authentication/Login.bru
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
meta {
|
||||||
|
name: Login
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{Host}}/v1/auth/login
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"username": "{{Username}}",
|
||||||
|
"password": "{{Password}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
username: {{Username}}
|
||||||
|
password: {{Password}}
|
||||||
|
}
|
17
BrunoColllection/Galaeth/Authentication/Refresh.bru
Normal file
17
BrunoColllection/Galaeth/Authentication/Refresh.bru
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
meta {
|
||||||
|
name: Refresh
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{Host}}/v1/auth/refresh
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"refreshToken": "Y1oMF6M9tNjTa2aX01lkXffJo7CrKCqwJ7bOxsjjgEXozVEhUFvNq7KydSQY5rPY"
|
||||||
|
}
|
||||||
|
}
|
15
BrunoColllection/Galaeth/Me/Me.bru
Normal file
15
BrunoColllection/Galaeth/Me/Me.bru
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
meta {
|
||||||
|
name: Me
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{Host}}/v1/me
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
x-api-key: !eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyLWlkIjoiYWQyZjI2MmUtOTVhMi00MDUzLTlmMjMtNmMxNjg5NWEwNGE3Iiwicm9sZSI6WyJOb3JtYWwiLCJNb2RlcmF0b3IiLCJSb290Il0sIm5iZiI6MTczMTc4MTEyMywiZXhwIjoxNzMxNzgxNDIzLCJpYXQiOjE3MzE3ODExMjN9.pqPt1YDZFsompRT_xGW-UT2SYQBaiNlbu3W6rgg_7S4
|
||||||
|
}
|
9
BrunoColllection/Galaeth/bruno.json
Normal file
9
BrunoColllection/Galaeth/bruno.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"name": "Galaeth",
|
||||||
|
"type": "collection",
|
||||||
|
"ignore": [
|
||||||
|
"node_modules",
|
||||||
|
".git"
|
||||||
|
]
|
||||||
|
}
|
7
BrunoColllection/Galaeth/environments/Galaeth.bru
Normal file
7
BrunoColllection/Galaeth/environments/Galaeth.bru
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
vars {
|
||||||
|
Host: http://localhost:5000
|
||||||
|
}
|
||||||
|
vars:secret [
|
||||||
|
Username,
|
||||||
|
Password
|
||||||
|
]
|
23
Dockerfile
Normal file
23
Dockerfile
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||||
|
USER $APP_UID
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 8080
|
||||||
|
EXPOSE 8081
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["Galaeth.CoreApi/Galaeth.CoreApi.csproj", "Galaeth.CoreApi/"]
|
||||||
|
RUN dotnet restore "Galaeth.CoreApi/Galaeth.CoreApi.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/Galaeth.CoreApi"
|
||||||
|
RUN dotnet build "Galaeth.CoreApi.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
RUN dotnet publish "Galaeth.CoreApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "Galaeth.CoreApi.dll"]
|
32
Galaeth.ApiServer/Constants/ApiErrorCodes.cs
Normal file
32
Galaeth.ApiServer/Constants/ApiErrorCodes.cs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
namespace Galaeth.ApiServer.Constants;
|
||||||
|
|
||||||
|
/// <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";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsupported user agent.
|
||||||
|
/// </summary>
|
||||||
|
public const string UnsupportedUserAgent = "unsupported-user-agent";
|
||||||
|
}
|
107
Galaeth.ApiServer/Controllers/ApiController.cs
Normal file
107
Galaeth.ApiServer/Controllers/ApiController.cs
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using Galaeth.ApiServer.Models.Common;
|
||||||
|
using Galaeth.Core.Constants;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Galaeth.ApiServer.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base Api Controller.
|
||||||
|
/// </summary>
|
||||||
|
[Consumes("application/json")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class ApiController : ControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Return a failure status with no data.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Instance of <see cref="IActionResult"/>.</returns>
|
||||||
|
protected static IActionResult FailureResult()
|
||||||
|
{
|
||||||
|
return new JsonResult(new ResultModel
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return a failure result with additional information.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="errorCode">Api error code.</param>
|
||||||
|
/// <param name="message">Api error message.</param>
|
||||||
|
/// <returns>Instance of <see cref="IActionResult"/>.</returns>
|
||||||
|
protected static IActionResult FailureResult(string errorCode, string message)
|
||||||
|
{
|
||||||
|
return new JsonResult(new ErrorResultModel
|
||||||
|
{
|
||||||
|
Error = errorCode,
|
||||||
|
Message = message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a failure result indicating the body is missing from the request.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Instance of <see cref="IActionResult"/>.</returns>
|
||||||
|
protected static IActionResult MissingBodyResult()
|
||||||
|
{
|
||||||
|
return FailureResult(CoreErrorCodes.NoDataProvided, "Missing request body");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return a success status with no data.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Instance of <see cref="IActionResult"/>.</returns>
|
||||||
|
protected static IActionResult SuccessResult()
|
||||||
|
{
|
||||||
|
return new JsonResult(new ResultModel
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return a success status with data.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Instance of <see cref="IActionResult"/>.</returns>
|
||||||
|
/// <typeparam name="TDataType">The primary key type.</typeparam>
|
||||||
|
/// <param name="data">The data to return in the result.</param>
|
||||||
|
protected static IActionResult SuccessResult<TDataType>(TDataType data)
|
||||||
|
where TDataType : class
|
||||||
|
{
|
||||||
|
return new JsonResult(new DataResultModel<TDataType>
|
||||||
|
{
|
||||||
|
Data = data,
|
||||||
|
Success = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetch IP address of requesting agent.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Request origin's IP address.</returns>
|
||||||
|
protected IPAddress ClientIpAddress()
|
||||||
|
{
|
||||||
|
IPAddress remoteIpAddress = null;
|
||||||
|
if (Request.Headers.TryGetValue("X-Forwarded-For", out var value))
|
||||||
|
{
|
||||||
|
foreach (var ip in value)
|
||||||
|
{
|
||||||
|
if (IPAddress.TryParse(ip, out var address) &&
|
||||||
|
(address.AddressFamily is AddressFamily.InterNetwork
|
||||||
|
or AddressFamily.InterNetworkV6))
|
||||||
|
{
|
||||||
|
remoteIpAddress = address;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
remoteIpAddress = HttpContext.Connection.RemoteIpAddress?.MapToIPv4();
|
||||||
|
}
|
||||||
|
|
||||||
|
return remoteIpAddress;
|
||||||
|
}
|
||||||
|
}
|
128
Galaeth.ApiServer/Controllers/AuthenticationController.cs
Normal file
128
Galaeth.ApiServer/Controllers/AuthenticationController.cs
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
using Galaeth.ApiServer.Constants;
|
||||||
|
using Galaeth.ApiServer.Models;
|
||||||
|
using Galaeth.Services.Dtos;
|
||||||
|
using Galaeth.Services.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using MyCSharp.HttpUserAgentParser.AspNetCore;
|
||||||
|
using Toycloud.AspNetCore.Mvc.ModelBinding;
|
||||||
|
|
||||||
|
namespace Galaeth.ApiServer.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authentication API endpoints.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
[Route("v1/auth")]
|
||||||
|
public class AuthenticationController : ApiController
|
||||||
|
{
|
||||||
|
private readonly IAuthenticationService _authenticationService;
|
||||||
|
private readonly IHttpUserAgentParserAccessor _httpUserAgentParserAccessor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AuthenticationController"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="authenticationService">Instance of <see cref="IAuthenticationService"/>.</param>
|
||||||
|
/// <param name="httpUserAgentParserAccessor">Instance of <see cref="IHttpUserAgentParserAccessor"/>.</param>
|
||||||
|
public AuthenticationController(
|
||||||
|
IAuthenticationService authenticationService,
|
||||||
|
IHttpUserAgentParserAccessor httpUserAgentParserAccessor)
|
||||||
|
{
|
||||||
|
_authenticationService = authenticationService;
|
||||||
|
_httpUserAgentParserAccessor = httpUserAgentParserAccessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process an authentication (login) request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Instance of <see cref="AuthenticateRequest"/>.</param>
|
||||||
|
/// <returns>If successful, instance of <see cref="AccessTokensDto"/>.</returns>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("login")]
|
||||||
|
public async Task<IActionResult> Authenticate([FromBodyOrDefault] AuthenticateRequest request)
|
||||||
|
{
|
||||||
|
if (request is null)
|
||||||
|
{
|
||||||
|
return MissingBodyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
var userAgent = _httpUserAgentParserAccessor.Get(Request.HttpContext);
|
||||||
|
|
||||||
|
if (userAgent is null)
|
||||||
|
{
|
||||||
|
return FailureResult(ApiErrorCodes.UnsupportedUserAgent, "Could not determine user agent");
|
||||||
|
}
|
||||||
|
|
||||||
|
var session = await _authenticationService.AuthenticateUser(new AuthenticateUserDto()
|
||||||
|
{
|
||||||
|
Username = request.Username,
|
||||||
|
Password = request.Password,
|
||||||
|
IpAddress = ClientIpAddress(),
|
||||||
|
UserAgent = userAgent.Value,
|
||||||
|
});
|
||||||
|
|
||||||
|
return SuccessResult(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process a refresh token request for user authentication.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userAuthRequest">Instance of <see cref="RefreshUserAuthRequest"/>.</param>
|
||||||
|
/// <returns>If successful, instance of <see cref="AccessTokensDto"/>.</returns>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("refresh")]
|
||||||
|
public async Task<IActionResult> RefreshSession([FromBodyOrDefault] RefreshUserAuthRequest userAuthRequest)
|
||||||
|
{
|
||||||
|
if (userAuthRequest is null)
|
||||||
|
{
|
||||||
|
return MissingBodyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
var userAgent = _httpUserAgentParserAccessor.Get(Request.HttpContext);
|
||||||
|
|
||||||
|
if (userAgent is null)
|
||||||
|
{
|
||||||
|
return FailureResult(ApiErrorCodes.UnsupportedUserAgent, "Could not determine user agent");
|
||||||
|
}
|
||||||
|
|
||||||
|
var session = await _authenticationService.AuthenticateUser(new RefreshAuthenticationDto()
|
||||||
|
{
|
||||||
|
RefreshToken = userAuthRequest.RefreshToken,
|
||||||
|
IpAddress = ClientIpAddress(),
|
||||||
|
UserAgent = userAgent.Value,
|
||||||
|
});
|
||||||
|
|
||||||
|
return SuccessResult(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User's request to change their own password.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Instance of <see cref="ChangePasswordRequest"/>.</param>
|
||||||
|
[HttpPost("pwd")]
|
||||||
|
public async Task<IActionResult> ChangePassword([FromBodyOrDefault] ChangePasswordRequest request)
|
||||||
|
{
|
||||||
|
if (request is null)
|
||||||
|
{
|
||||||
|
return MissingBodyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
var userAgent = _httpUserAgentParserAccessor.Get(Request.HttpContext);
|
||||||
|
|
||||||
|
if (userAgent is null)
|
||||||
|
{
|
||||||
|
return FailureResult(ApiErrorCodes.UnsupportedUserAgent, "Could not determine user agent");
|
||||||
|
}
|
||||||
|
|
||||||
|
var newTokens = await _authenticationService.ChangeUserPassword(new ChangePasswordDto()
|
||||||
|
{
|
||||||
|
OldPassword = request.OldPassword,
|
||||||
|
NewPassword = request.NewPassword,
|
||||||
|
UserAgent = userAgent.Value,
|
||||||
|
IpAddress = ClientIpAddress(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return SuccessResult(newTokens);
|
||||||
|
}
|
||||||
|
}
|
44
Galaeth.ApiServer/Controllers/UserSelfController.cs
Normal file
44
Galaeth.ApiServer/Controllers/UserSelfController.cs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
using Galaeth.Core.Dtos;
|
||||||
|
using Galaeth.Services.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Galaeth.ApiServer.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Api methods for the requesting authorised user.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
[Route("v1/me")]
|
||||||
|
public class UserSelfController : ApiController
|
||||||
|
{
|
||||||
|
private readonly IIdentityProvider _identityProvider;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="UserSelfController"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="identityProvider">Instance of <see cref="IIdentityProvider"/>.</param>
|
||||||
|
/// <param name="userService">Instance of <see cref="IUserService"/>.</param>
|
||||||
|
public UserSelfController(
|
||||||
|
IIdentityProvider identityProvider,
|
||||||
|
IUserService userService)
|
||||||
|
{
|
||||||
|
_identityProvider = identityProvider;
|
||||||
|
_userService = userService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the requesting user information and return it.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Instance of <see cref="UserDto"/>.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetMe()
|
||||||
|
{
|
||||||
|
var userId = _identityProvider.GetRequestingUserId();
|
||||||
|
var user = await _userService.FindById(userId);
|
||||||
|
|
||||||
|
return SuccessResult(user);
|
||||||
|
}
|
||||||
|
}
|
32
Galaeth.ApiServer/Extensions/JwtConfigurationExtensions.cs
Normal file
32
Galaeth.ApiServer/Extensions/JwtConfigurationExtensions.cs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
using System.Text;
|
||||||
|
using Galaeth.Core.Configuration;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace Galaeth.ApiServer.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extensions for <see cref="JwtConfiguration"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static class JwtConfigurationExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Generate an instance of <see cref="TokenValidationParameters"/> from <see cref="JwtConfiguration"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="config">Instance of <see cref="JwtConfiguration"/>.</param>
|
||||||
|
/// <returns>Instance of <see cref="TokenValidationParameters"/>.</returns>
|
||||||
|
public static TokenValidationParameters ToTokenValidationParameters(this JwtConfiguration 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)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
35
Galaeth.ApiServer/Galaeth.ApiServer.csproj
Normal file
35
Galaeth.ApiServer/Galaeth.ApiServer.csproj
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<CodeAnalysisRuleSet>../Galaeth.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="..\.dockerignore">
|
||||||
|
<Link>.dockerignore</Link>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<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.10-g39a7b02192" />
|
||||||
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||||
|
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
|
||||||
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Toycloud.AspNetCore.Mvc.ModelBinding.BodyOrDefaultBinding" Version="1.2.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Galaeth.Core\Galaeth.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\Galaeth.Services\Galaeth.Services.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
58
Galaeth.ApiServer/HostedServices/StartupService.cs
Normal file
58
Galaeth.ApiServer/HostedServices/StartupService.cs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
using Galaeth.Core.Infrastructure;
|
||||||
|
using Galaeth.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace Galaeth.ApiServer.HostedServices;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes to be executed at startup.
|
||||||
|
/// </summary>
|
||||||
|
public class StartupService : IHostedService
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
|
private readonly ILogger<StartupService> _logger;
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
}
|
107
Galaeth.ApiServer/Middleware/ExceptionMiddleware.cs
Normal file
107
Galaeth.ApiServer/Middleware/ExceptionMiddleware.cs
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Net;
|
||||||
|
using Galaeth.ApiServer.Constants;
|
||||||
|
using Galaeth.ApiServer.Models.Common;
|
||||||
|
using Galaeth.Core.Constants;
|
||||||
|
using Galaeth.Core.Exceptions;
|
||||||
|
|
||||||
|
namespace Galaeth.ApiServer.Middleware;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle exceptions caught in a request.
|
||||||
|
/// </summary>
|
||||||
|
public class ExceptionMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<ExceptionMiddleware> _logger;
|
||||||
|
|
||||||
|
/// <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()
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Error = ApiErrorCodes.UnknownError,
|
||||||
|
Message = "Something went wrong. Try again later",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
90
Galaeth.ApiServer/Middleware/StatusCodeMiddleware.cs
Normal file
90
Galaeth.ApiServer/Middleware/StatusCodeMiddleware.cs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
using Galaeth.ApiServer.Constants;
|
||||||
|
using Galaeth.Core.Constants;
|
||||||
|
|
||||||
|
namespace Galaeth.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) =>
|
||||||
|
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
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
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
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Error = ApiErrorCodes.UnknownMethod,
|
||||||
|
Message = "Unknown method",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 405:
|
||||||
|
context.Response.Headers.Clear();
|
||||||
|
context.Response.ContentType = "text/json";
|
||||||
|
await context.Response.WriteAsJsonAsync(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Error = ApiErrorCodes.IllegalMethod,
|
||||||
|
Message = "Illegal method",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 415:
|
||||||
|
context.Response.Headers.Clear();
|
||||||
|
context.Response.ContentType = "text/json";
|
||||||
|
await context.Response.WriteAsJsonAsync(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
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
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Error = ApiErrorCodes.UnknownError,
|
||||||
|
Message = "Unknown error",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
Galaeth.ApiServer/Models/AuthenticateRequest.cs
Normal file
21
Galaeth.ApiServer/Models/AuthenticateRequest.cs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Galaeth.ApiServer.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authentication request.
|
||||||
|
/// </summary>
|
||||||
|
public class AuthenticateRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Agent's username.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("username")]
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Agent's password.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("password")]
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
17
Galaeth.ApiServer/Models/ChangePasswordRequest.cs
Normal file
17
Galaeth.ApiServer/Models/ChangePasswordRequest.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
namespace Galaeth.ApiServer.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change password request model.
|
||||||
|
/// </summary>
|
||||||
|
public class ChangePasswordRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The user's old password.
|
||||||
|
/// </summary>
|
||||||
|
public string OldPassword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user's new password.
|
||||||
|
/// </summary>
|
||||||
|
public string NewPassword { get; set; }
|
||||||
|
}
|
17
Galaeth.ApiServer/Models/Common/DataResultModel.cs
Normal file
17
Galaeth.ApiServer/Models/Common/DataResultModel.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Galaeth.ApiServer.Models.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result with data model.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TDataType">The data type being returned.</typeparam>
|
||||||
|
public class DataResultModel<TDataType> : ResultModel
|
||||||
|
where TDataType : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Included data payload.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public TDataType Data { get; set; }
|
||||||
|
}
|
21
Galaeth.ApiServer/Models/Common/ErrorResultModel.cs
Normal file
21
Galaeth.ApiServer/Models/Common/ErrorResultModel.cs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Galaeth.ApiServer.Models.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Error result model.
|
||||||
|
/// </summary>
|
||||||
|
public class ErrorResultModel : ResultModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Error code.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("error")]
|
||||||
|
public string Error { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Error message.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public string Message { get; set; }
|
||||||
|
}
|
15
Galaeth.ApiServer/Models/Common/ResultModel.cs
Normal file
15
Galaeth.ApiServer/Models/Common/ResultModel.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Galaeth.ApiServer.Models.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Basic result model.
|
||||||
|
/// </summary>
|
||||||
|
public class ResultModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// True if successful, false otherwise.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("success")]
|
||||||
|
public bool Success { get; set; }
|
||||||
|
}
|
15
Galaeth.ApiServer/Models/RefreshUserAuthRequest.cs
Normal file
15
Galaeth.ApiServer/Models/RefreshUserAuthRequest.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Galaeth.ApiServer.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refresh session with refresh token request.
|
||||||
|
/// </summary>
|
||||||
|
public class RefreshUserAuthRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Refresh token.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("refreshToken")]
|
||||||
|
public string RefreshToken { get; set; }
|
||||||
|
}
|
91
Galaeth.ApiServer/Program.cs
Normal file
91
Galaeth.ApiServer/Program.cs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
using Galaeth.ApiServer.Extensions;
|
||||||
|
using Galaeth.ApiServer.HostedServices;
|
||||||
|
using Galaeth.ApiServer.Middleware;
|
||||||
|
using Galaeth.Core.Configuration;
|
||||||
|
using Galaeth.Services.Configuration;
|
||||||
|
using Galaeth.Services.Profiles;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using MyCSharp.HttpUserAgentParser.AspNetCore.DependencyInjection;
|
||||||
|
using MyCSharp.HttpUserAgentParser.MemoryCache.DependencyInjection;
|
||||||
|
using Serilog;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace Galaeth.ApiServer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entry point for service.
|
||||||
|
/// </summary>
|
||||||
|
internal static class Program
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Entry point method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Service arguments.</param>
|
||||||
|
public static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Configuration
|
||||||
|
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
||||||
|
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
|
||||||
|
.AddEnvironmentVariables();
|
||||||
|
|
||||||
|
Log.Logger = new LoggerConfiguration()
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
.MinimumLevel.Override("Default", LogEventLevel.Warning)
|
||||||
|
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||||
|
.MinimumLevel.Override("System", LogEventLevel.Warning)
|
||||||
|
.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.AddGalaethCore();
|
||||||
|
builder.Services.AddGalaethApiServer();
|
||||||
|
builder.Services.AddGalaethServices();
|
||||||
|
builder.Services.AddGalaethDAL();
|
||||||
|
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
|
// Setup authentication.
|
||||||
|
var jwtConfig = new JwtConfiguration();
|
||||||
|
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<DatabaseConfiguration>(builder.Configuration.GetSection("Database"));
|
||||||
|
builder.Services.Configure<PwdHashConfiguration>(builder.Configuration.GetSection("PwdHash"));
|
||||||
|
builder.Services.Configure<InitialUserConfiguration>(builder.Configuration.GetSection("InitialUser"));
|
||||||
|
builder.Services.Configure<RegistrationConfiguration>(builder.Configuration.GetSection("Registration"));
|
||||||
|
builder.Services.Configure<EmailDomainBlacklistConfiguration>(
|
||||||
|
builder.Configuration.GetSection("EmailDomainBlacklist"));
|
||||||
|
builder.Services.Configure<JwtConfiguration>(builder.Configuration.GetSection("JWT"));
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Error handling middleware.
|
||||||
|
app.UseMiddleware<ExceptionMiddleware>();
|
||||||
|
app.UseMiddleware<StatusCodeMiddleware>();
|
||||||
|
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
await app.RunAsync();
|
||||||
|
}
|
||||||
|
}
|
34
Galaeth.ApiServer/Providers/IdentityProvider.cs
Normal file
34
Galaeth.ApiServer/Providers/IdentityProvider.cs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
using Galaeth.Services.Constants;
|
||||||
|
using Galaeth.Services.Interfaces;
|
||||||
|
using Injectio.Attributes;
|
||||||
|
|
||||||
|
namespace Galaeth.ApiServer.Providers;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[RegisterScoped]
|
||||||
|
public class IdentityProvider : IIdentityProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="IdentityProvider"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpContextAccessor">Instance of <see cref="IHttpContextAccessor"/>.</param>
|
||||||
|
public IdentityProvider(IHttpContextAccessor httpContextAccessor)
|
||||||
|
{
|
||||||
|
var claims = httpContextAccessor.HttpContext?.User.Claims;
|
||||||
|
if (claims is not null)
|
||||||
|
{
|
||||||
|
UserId = claims.FirstOrDefault(c => c.Type == ClaimIds.UserId)?.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User's id.
|
||||||
|
/// </summary>
|
||||||
|
private string UserId { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetRequestingUserId()
|
||||||
|
{
|
||||||
|
return UserId;
|
||||||
|
}
|
||||||
|
}
|
41
Galaeth.ApiServer/appsettings.json
Normal file
41
Galaeth.ApiServer/appsettings.json
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"JWT": {
|
||||||
|
"SecretKey": "your-secret-key",
|
||||||
|
"Issuer": "your-issuer",
|
||||||
|
"Audience": "your-audience",
|
||||||
|
"AccessTokenExpiration": "5 mins",
|
||||||
|
"RefreshTokenExpiration": "3 days",
|
||||||
|
"ValidateIssuer": true,
|
||||||
|
"ValidateAudience": true
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
namespace Galaeth.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,22 @@
|
||||||
|
namespace Galaeth.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,22 @@
|
||||||
|
namespace Galaeth.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; }
|
||||||
|
}
|
12
Galaeth.Core/Configuration/DatabaseConfiguration.cs
Normal file
12
Galaeth.Core/Configuration/DatabaseConfiguration.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
namespace Galaeth.Core.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Database Configuration.
|
||||||
|
/// </summary>
|
||||||
|
public class DatabaseConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Connection string.
|
||||||
|
/// </summary>
|
||||||
|
public string ConnectionString { get; init; }
|
||||||
|
}
|
42
Galaeth.Core/Configuration/JwtConfiguration.cs
Normal file
42
Galaeth.Core/Configuration/JwtConfiguration.cs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
namespace Galaeth.Core.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Jwt Authentication Configuration.
|
||||||
|
/// </summary>
|
||||||
|
public class JwtConfiguration
|
||||||
|
{
|
||||||
|
/// <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; }
|
||||||
|
}
|
22
Galaeth.Core/Constants/CoreErrorCodes.cs
Normal file
22
Galaeth.Core/Constants/CoreErrorCodes.cs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
namespace Galaeth.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";
|
||||||
|
}
|
14
Galaeth.Core/Constants/RefreshTokenContext.cs
Normal file
14
Galaeth.Core/Constants/RefreshTokenContext.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
using Galaeth.Core.Entities;
|
||||||
|
|
||||||
|
namespace Galaeth.Core.Constants;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contexts for <see cref="RefreshToken"/> entities.
|
||||||
|
/// </summary>
|
||||||
|
public enum RefreshTokenContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Refresh token for a user's access.
|
||||||
|
/// </summary>
|
||||||
|
UserAccess,
|
||||||
|
}
|
22
Galaeth.Core/Constants/UserRole.cs
Normal file
22
Galaeth.Core/Constants/UserRole.cs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
namespace Galaeth.Core.Constants;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User's role.
|
||||||
|
/// </summary>
|
||||||
|
public enum UserRole
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Normal user.
|
||||||
|
/// </summary>
|
||||||
|
Normal,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instance moderator.
|
||||||
|
/// </summary>
|
||||||
|
Moderator,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Root account.
|
||||||
|
/// </summary>
|
||||||
|
Root,
|
||||||
|
}
|
27
Galaeth.Core/Constants/UserState.cs
Normal file
27
Galaeth.Core/Constants/UserState.cs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
namespace Galaeth.Core.Constants;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Available user states.
|
||||||
|
/// </summary>
|
||||||
|
public enum UserState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Normal activated user.
|
||||||
|
/// </summary>
|
||||||
|
Normal,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Suspended account.
|
||||||
|
/// </summary>
|
||||||
|
Suspended,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Banned account.
|
||||||
|
/// </summary>
|
||||||
|
Banned,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Awaiting email activation.
|
||||||
|
/// </summary>
|
||||||
|
AwaitingEmailActivation,
|
||||||
|
}
|
44
Galaeth.Core/Dtos/UserDto.cs
Normal file
44
Galaeth.Core/Dtos/UserDto.cs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
using Galaeth.Core.Constants;
|
||||||
|
|
||||||
|
namespace Galaeth.Core.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dto representing a user.
|
||||||
|
/// </summary>
|
||||||
|
public class UserDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// User's Id.
|
||||||
|
/// </summary>
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User's name.
|
||||||
|
/// </summary>
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User's email address.
|
||||||
|
/// </summary>
|
||||||
|
public string EmailAddress { 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>
|
||||||
|
/// User's role.
|
||||||
|
/// </summary>
|
||||||
|
public UserRole Role { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User's role as title.
|
||||||
|
/// </summary>
|
||||||
|
public string RoleTitle { get; set; }
|
||||||
|
}
|
23
Galaeth.Core/Entities/EmailDomainBlacklist.cs
Normal file
23
Galaeth.Core/Entities/EmailDomainBlacklist.cs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
using Galaeth.Core.Attributes.EntityAnnotation;
|
||||||
|
|
||||||
|
namespace Galaeth.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; }
|
||||||
|
}
|
61
Galaeth.Core/Entities/RefreshToken.cs
Normal file
61
Galaeth.Core/Entities/RefreshToken.cs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
using Galaeth.Core.Attributes.EntityAnnotation;
|
||||||
|
using Galaeth.Core.Constants;
|
||||||
|
using MyCSharp.HttpUserAgentParser;
|
||||||
|
|
||||||
|
namespace Galaeth.Core.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entity representing a refresh token.
|
||||||
|
/// </summary>
|
||||||
|
[TableMapping("refreshTokens")]
|
||||||
|
public class RefreshToken
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The token.
|
||||||
|
/// </summary>
|
||||||
|
[ColumnMapping("token")]
|
||||||
|
[PrimaryKey]
|
||||||
|
public string Token { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Token context.
|
||||||
|
/// </summary>
|
||||||
|
[ColumnMapping("tokenContext")]
|
||||||
|
public RefreshTokenContext TokenContext { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The owner user's id.
|
||||||
|
/// </summary>
|
||||||
|
[ColumnMapping("userId")]
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the token expires.
|
||||||
|
/// </summary>
|
||||||
|
[ColumnMapping("expires")]
|
||||||
|
public DateTime Expires { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user agent this refresh token belongs to.
|
||||||
|
/// </summary>
|
||||||
|
[ColumnMapping("userAgent")]
|
||||||
|
public string UserAgent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User agent type.
|
||||||
|
/// </summary>
|
||||||
|
[ColumnMapping("userAgentType")]
|
||||||
|
public HttpUserAgentType UserAgentType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ip address this refresh token belongs to.
|
||||||
|
/// </summary>
|
||||||
|
[ColumnMapping("ipAddress")]
|
||||||
|
public string IpAddress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the token was created.
|
||||||
|
/// </summary>
|
||||||
|
[ColumnMapping("createdAt")]
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
78
Galaeth.Core/Entities/User.cs
Normal file
78
Galaeth.Core/Entities/User.cs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
using Galaeth.Core.Attributes.EntityAnnotation;
|
||||||
|
using Galaeth.Core.Constants;
|
||||||
|
|
||||||
|
namespace Galaeth.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 account's role.
|
||||||
|
/// </summary>
|
||||||
|
[ColumnMapping("role")]
|
||||||
|
public UserRole Role { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// State of the user's account.
|
||||||
|
/// </summary>
|
||||||
|
[ColumnMapping("state")]
|
||||||
|
public UserState State { get; set; }
|
||||||
|
}
|
17
Galaeth.Core/Exceptions/MigrationException.cs
Normal file
17
Galaeth.Core/Exceptions/MigrationException.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
namespace Galaeth.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}")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
22
Galaeth.Core/Exceptions/MissingColumnAttributeException.cs
Normal file
22
Galaeth.Core/Exceptions/MissingColumnAttributeException.cs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
namespace Galaeth.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; }
|
||||||
|
}
|
22
Galaeth.Core/Exceptions/MultiplePrimaryKeysException.cs
Normal file
22
Galaeth.Core/Exceptions/MultiplePrimaryKeysException.cs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
namespace Galaeth.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; }
|
||||||
|
}
|
24
Galaeth.Core/Exceptions/PrimaryKeyMissingException.cs
Normal file
24
Galaeth.Core/Exceptions/PrimaryKeyMissingException.cs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
using Galaeth.Core.Attributes.EntityAnnotation;
|
||||||
|
|
||||||
|
namespace Galaeth.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; }
|
||||||
|
}
|
50
Galaeth.Core/Exceptions/ServiceException.cs
Normal file
50
Galaeth.Core/Exceptions/ServiceException.cs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Galaeth.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>
|
||||||
|
public ServiceException(string errorCode, string errorMessage)
|
||||||
|
: base(errorMessage)
|
||||||
|
{
|
||||||
|
ErrorCode = errorCode;
|
||||||
|
ErrorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
public 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; } = HttpStatusCode.BadRequest;
|
||||||
|
}
|
30
Galaeth.Core/Extensions/JwtConfigurationExtensions.cs
Normal file
30
Galaeth.Core/Extensions/JwtConfigurationExtensions.cs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
using Galaeth.Core.Configuration;
|
||||||
|
using TimeSpanParserUtil;
|
||||||
|
|
||||||
|
namespace Galaeth.Core.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extensions for <see cref="JwtConfiguration"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static class JwtConfigurationExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Return when (UTC) the access token expires.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configuration">Instance of <see cref="JwtConfiguration"/>.</param>
|
||||||
|
/// <returns>A UTC DateTime of when the expiration occurs.</returns>
|
||||||
|
public static DateTime AccessTokenExpirationDateTime(this JwtConfiguration 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="JwtConfiguration"/>.</param>
|
||||||
|
/// <returns>A UTC DateTime of when the expiration occurs.</returns>
|
||||||
|
public static DateTime RefreshTokenExpirationDateTime(this JwtConfiguration configuration)
|
||||||
|
{
|
||||||
|
return DateTime.UtcNow.Add(TimeSpanParser.Parse(configuration.RefreshTokenExpiration));
|
||||||
|
}
|
||||||
|
}
|
23
Galaeth.Core/Extensions/ListExtensions.cs
Normal file
23
Galaeth.Core/Extensions/ListExtensions.cs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
namespace Galaeth.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();
|
||||||
|
}
|
||||||
|
}
|
32
Galaeth.Core/Extensions/StringExtensions.cs
Normal file
32
Galaeth.Core/Extensions/StringExtensions.cs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
namespace Galaeth.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) ..]}";
|
||||||
|
}
|
||||||
|
}
|
31
Galaeth.Core/Extensions/UserRoleExtensions.cs
Normal file
31
Galaeth.Core/Extensions/UserRoleExtensions.cs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
using Galaeth.Core.Constants;
|
||||||
|
|
||||||
|
namespace Galaeth.Core.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="UserRole"/> extensions.
|
||||||
|
/// </summary>
|
||||||
|
public static class UserRoleExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Return a collection of roles a role is entitled to.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="role">The role to query.</param>
|
||||||
|
/// <returns>A collection of roles the role is entitled to.</returns>
|
||||||
|
public static UserRole[] UserRoleEntitlement(this UserRole role)
|
||||||
|
{
|
||||||
|
switch (role)
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
case UserRole.Normal:
|
||||||
|
return
|
||||||
|
[UserRole.Normal];
|
||||||
|
case UserRole.Moderator:
|
||||||
|
return
|
||||||
|
[UserRole.Normal, UserRole.Moderator];
|
||||||
|
case UserRole.Root:
|
||||||
|
return
|
||||||
|
[UserRole.Normal, UserRole.Moderator, UserRole.Root];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
Galaeth.Core/Galaeth.Core.csproj
Normal file
21
Galaeth.Core/Galaeth.Core.csproj
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<CodeAnalysisRuleSet>../Galaeth.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||||
|
<PackageReference Include="FluentValidation" Version="11.10.0" />
|
||||||
|
<PackageReference Include="Injectio" Version="3.3.0" />
|
||||||
|
<PackageReference Include="MyCSharp.HttpUserAgentParser" Version="3.0.9" />
|
||||||
|
<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>
|
14
Galaeth.Core/Infrastructure/IDataAccessLayerSetup.cs
Normal file
14
Galaeth.Core/Infrastructure/IDataAccessLayerSetup.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
using Injectio.Attributes;
|
||||||
|
|
||||||
|
namespace Galaeth.Core.Infrastructure;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Perform any setup steps required for the data access layer.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDataAccessLayerSetup
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Setup routine.
|
||||||
|
/// </summary>
|
||||||
|
Task Setup();
|
||||||
|
}
|
13
Galaeth.Core/Infrastructure/IDatabaseMigrator.cs
Normal file
13
Galaeth.Core/Infrastructure/IDatabaseMigrator.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
namespace Galaeth.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");
|
||||||
|
}
|
21
Galaeth.Core/Infrastructure/IDbConnectionProvider.cs
Normal file
21
Galaeth.Core/Infrastructure/IDbConnectionProvider.cs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
using System.Data;
|
||||||
|
|
||||||
|
namespace Galaeth.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();
|
||||||
|
}
|
24
Galaeth.Core/Infrastructure/ITransactionProvider.cs
Normal file
24
Galaeth.Core/Infrastructure/ITransactionProvider.cs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
using System.Data;
|
||||||
|
using System.Data.Common;
|
||||||
|
|
||||||
|
namespace Galaeth.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);
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
using Galaeth.Core.Entities;
|
||||||
|
|
||||||
|
namespace Galaeth.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();
|
||||||
|
}
|
50
Galaeth.Core/RepositoryInterfaces/IGenericRepository.cs
Normal file
50
Galaeth.Core/RepositoryInterfaces/IGenericRepository.cs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
namespace Galaeth.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);
|
||||||
|
}
|
15
Galaeth.Core/RepositoryInterfaces/IRefreshTokenRepository.cs
Normal file
15
Galaeth.Core/RepositoryInterfaces/IRefreshTokenRepository.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
using Galaeth.Core.Entities;
|
||||||
|
|
||||||
|
namespace Galaeth.Core.RepositoryInterfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="RefreshToken"/> Repository.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRefreshTokenRepository : IGenericRepository<RefreshToken, string>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Revoke all tokens for the provided user id.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">User id.</param>
|
||||||
|
Task RevokeAllForUser(Guid userId);
|
||||||
|
}
|
29
Galaeth.Core/RepositoryInterfaces/IUserRepository.cs
Normal file
29
Galaeth.Core/RepositoryInterfaces/IUserRepository.cs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
using Galaeth.Core.Entities;
|
||||||
|
|
||||||
|
namespace Galaeth.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);
|
||||||
|
}
|
28
Galaeth.DAL/Galaeth.DAL.csproj
Normal file
28
Galaeth.DAL/Galaeth.DAL.csproj
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<CodeAnalysisRuleSet>../Galaeth.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Galaeth.Core\Galaeth.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||||
|
<PackageReference Include="Npgsql" Version="8.0.5" />
|
||||||
|
<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>
|
49
Galaeth.DAL/Infrastructure/DataAccessLayerSetup.cs
Normal file
49
Galaeth.DAL/Infrastructure/DataAccessLayerSetup.cs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
using Galaeth.Core.Attributes.EntityAnnotation;
|
||||||
|
using Galaeth.Core.Infrastructure;
|
||||||
|
using Injectio.Attributes;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Galaeth.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");
|
||||||
|
|
||||||
|
Dapper.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)
|
||||||
|
{
|
||||||
|
Dapper.SqlMapper.SetTypeMap(entity, new EntityAttributeTypeMapper(entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
90
Galaeth.DAL/Infrastructure/DatabaseMigrator.cs
Normal file
90
Galaeth.DAL/Infrastructure/DatabaseMigrator.cs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
using Dapper;
|
||||||
|
using Galaeth.Core.Exceptions;
|
||||||
|
using Galaeth.Core.Infrastructure;
|
||||||
|
using Injectio.Attributes;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Galaeth.DAL.Infrastructure;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[RegisterScoped]
|
||||||
|
public class DatabaseMigrator : IDatabaseMigrator
|
||||||
|
{
|
||||||
|
private readonly IDbConnectionProvider _dbConnectionProvider;
|
||||||
|
private readonly ITransactionProvider _transactionProvider;
|
||||||
|
private readonly ILogger<DatabaseMigrator> _logger;
|
||||||
|
|
||||||
|
/// <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");
|
||||||
|
}
|
||||||
|
}
|
95
Galaeth.DAL/Infrastructure/DbConnectionProvider.cs
Normal file
95
Galaeth.DAL/Infrastructure/DbConnectionProvider.cs
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
using System.Data;
|
||||||
|
using Galaeth.Core.Configuration;
|
||||||
|
using Galaeth.Core.Infrastructure;
|
||||||
|
using Injectio.Attributes;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Npgsql;
|
||||||
|
|
||||||
|
namespace Galaeth.DAL.Infrastructure;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Galaeth.Core.Infrastructure.IDbConnectionProvider" />
|
||||||
|
[RegisterScoped]
|
||||||
|
public class DbConnectionProvider : IDbConnectionProvider, IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly IOptions<DatabaseConfiguration> _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{DatabaseConfiguration}"/>.</param>
|
||||||
|
/// <param name="logger">Instance of <see cref="ILogger"/>.</param>
|
||||||
|
public DbConnectionProvider(
|
||||||
|
IOptions<DatabaseConfiguration> databaseConfiguration,
|
||||||
|
ILogger<DbConnectionProvider> logger)
|
||||||
|
{
|
||||||
|
_databaseConfiguration = databaseConfiguration;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Galaeth.Core.Infrastructure.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 async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_dbConnection != null)
|
||||||
|
{
|
||||||
|
await _dbConnection.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_dbConnection is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Disposing database connection");
|
||||||
|
_dbConnection.Close();
|
||||||
|
_dbConnection.Dispose();
|
||||||
|
_dbConnection = null;
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
31
Galaeth.DAL/Infrastructure/EntityAttributeTypeMapper.cs
Normal file
31
Galaeth.DAL/Infrastructure/EntityAttributeTypeMapper.cs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using Dapper;
|
||||||
|
using Galaeth.Core.Attributes.EntityAnnotation;
|
||||||
|
|
||||||
|
namespace Galaeth.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)
|
||||||
|
])
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
110
Galaeth.DAL/Infrastructure/FallbackTypeMapper.cs
Normal file
110
Galaeth.DAL/Infrastructure/FallbackTypeMapper.cs
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using Dapper;
|
||||||
|
|
||||||
|
namespace Galaeth.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>
|
||||||
|
public 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 paramter 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;
|
||||||
|
}
|
||||||
|
}
|
35
Galaeth.DAL/Infrastructure/TransactionProvider.cs
Normal file
35
Galaeth.DAL/Infrastructure/TransactionProvider.cs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
using System.Data;
|
||||||
|
using Galaeth.Core.Infrastructure;
|
||||||
|
using Injectio.Attributes;
|
||||||
|
|
||||||
|
namespace Galaeth.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="Galaeth.Core.Infrastructure.ITransactionProvider"/>
|
||||||
|
public IDbTransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified)
|
||||||
|
{
|
||||||
|
var connection = _dbConnectionProvider.OpenConnection();
|
||||||
|
return connection.BeginTransaction(isolationLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Galaeth.Core.Infrastructure.ITransactionProvider"/>
|
||||||
|
public async Task<IDbTransaction> BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified)
|
||||||
|
{
|
||||||
|
var connection = await _dbConnectionProvider.OpenConnectionAsync();
|
||||||
|
return connection.BeginTransaction(isolationLevel);
|
||||||
|
}
|
||||||
|
}
|
9
Galaeth.DAL/Migrations/2024-11-13.01-initial.sql
Normal file
9
Galaeth.DAL/Migrations/2024-11-13.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;
|
||||||
|
$$;
|
20
Galaeth.DAL/Migrations/2024-11-13.02-users.sql
Normal file
20
Galaeth.DAL/Migrations/2024-11-13.02-users.sql
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
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,
|
||||||
|
state SMALLINT,
|
||||||
|
role SMALLINT,
|
||||||
|
lastLoggedIn TIMESTAMP DEFAULT NULL,
|
||||||
|
creatorIp TEXT DEFAULT '0.0.0.0'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER users_updated_at
|
||||||
|
BEFORE UPDATE
|
||||||
|
ON users
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE updated_at_timestamp();
|
|
@ -0,0 +1,5 @@
|
||||||
|
CREATE TABLE emailDomainBlacklist
|
||||||
|
(
|
||||||
|
domain TEXT UNIQUE PRIMARY KEY NOT NULL,
|
||||||
|
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
170423
Galaeth.DAL/Migrations/2024-11-15.02-emailDomainBlacklist-seed.sql
Normal file
170423
Galaeth.DAL/Migrations/2024-11-15.02-emailDomainBlacklist-seed.sql
Normal file
File diff suppressed because it is too large
Load diff
11
Galaeth.DAL/Migrations/2024-11-16.01-refreshTokens.sql
Normal file
11
Galaeth.DAL/Migrations/2024-11-16.01-refreshTokens.sql
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
CREATE TABLE refreshTokens
|
||||||
|
(
|
||||||
|
token TEXT UNIQUE PRIMARY KEY NOT NULL,
|
||||||
|
tokenContext SMALLINT NOT NULL,
|
||||||
|
userId UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
userAgent TEXT NOT NULL,
|
||||||
|
userAgentType SMALLINT NOT NULL,
|
||||||
|
ipAddress TEXT NOT NULL,
|
||||||
|
expires TIMESTAMP NOT NULL,
|
||||||
|
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
251
Galaeth.DAL/Repositories/BaseRepository.cs
Normal file
251
Galaeth.DAL/Repositories/BaseRepository.cs
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
using System.Data;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using Dapper;
|
||||||
|
using Galaeth.Core.Attributes.EntityAnnotation;
|
||||||
|
using Galaeth.Core.Exceptions;
|
||||||
|
using Galaeth.Core.Infrastructure;
|
||||||
|
using Galaeth.Core.RepositoryInterfaces;
|
||||||
|
|
||||||
|
namespace Galaeth.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="Galaeth.Core.RepositoryInterfaces.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="Galaeth.Core.RepositoryInterfaces.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="Galaeth.Core.RepositoryInterfaces.IGenericRepository{TEntity,TKeyType}" />
|
||||||
|
public async Task<TKeyType> AddAsync(TEntity entity)
|
||||||
|
{
|
||||||
|
return await WithDatabaseAsync(async connection =>
|
||||||
|
{
|
||||||
|
return await connection.QuerySingleAsync<TKeyType>(
|
||||||
|
InsertQuery, entity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Galaeth.Core.RepositoryInterfaces.IGenericRepository{TEntity,TKeyType}" />
|
||||||
|
public async Task AddAsync(IEnumerable<TEntity> entities)
|
||||||
|
{
|
||||||
|
await WithDatabaseAsync(async connection =>
|
||||||
|
{
|
||||||
|
await connection.ExecuteAsync(
|
||||||
|
InsertQuery, entities);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Galaeth.Core.RepositoryInterfaces.IGenericRepository{TEntity,TKeyType}" />
|
||||||
|
public async Task<bool> UpdateAsync(TEntity entity)
|
||||||
|
{
|
||||||
|
return await WithDatabaseAsync(async connection => await connection.ExecuteAsync(UpdateQuery, entity) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Galaeth.Core.RepositoryInterfaces.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;";
|
||||||
|
}
|
||||||
|
}
|
28
Galaeth.DAL/Repositories/EmailDomainBlacklistRepository.cs
Normal file
28
Galaeth.DAL/Repositories/EmailDomainBlacklistRepository.cs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
using Dapper;
|
||||||
|
using Galaeth.Core.Entities;
|
||||||
|
using Galaeth.Core.Infrastructure;
|
||||||
|
using Galaeth.Core.RepositoryInterfaces;
|
||||||
|
using Injectio.Attributes;
|
||||||
|
|
||||||
|
namespace Galaeth.DAL.Repositories;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Galaeth.Core.RepositoryInterfaces.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="Galaeth.Core.RepositoryInterfaces.IEmailDomainBlacklistRepository" />
|
||||||
|
public async Task<long> CountEntries()
|
||||||
|
{
|
||||||
|
return await WithDatabaseAsync(async connection =>
|
||||||
|
await connection.QueryFirstAsync<long>($"SELECT COUNT(*) FROM {Table};"));
|
||||||
|
}
|
||||||
|
}
|
31
Galaeth.DAL/Repositories/RefreshTokenRepository.cs
Normal file
31
Galaeth.DAL/Repositories/RefreshTokenRepository.cs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
using Dapper;
|
||||||
|
using Galaeth.Core.Entities;
|
||||||
|
using Galaeth.Core.Infrastructure;
|
||||||
|
using Galaeth.Core.RepositoryInterfaces;
|
||||||
|
using Injectio.Attributes;
|
||||||
|
|
||||||
|
namespace Galaeth.DAL.Repositories;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Galaeth.Core.RepositoryInterfaces.IRefreshTokenRepository" />
|
||||||
|
[RegisterScoped]
|
||||||
|
public class RefreshTokenRepository : BaseRepository<RefreshToken, string>, IRefreshTokenRepository
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="RefreshTokenRepository"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="db">Instance of <see cref="IDbConnectionProvider"/>.</param>
|
||||||
|
public RefreshTokenRepository(IDbConnectionProvider db)
|
||||||
|
: base(db)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Galaeth.Core.RepositoryInterfaces.IRefreshTokenRepository" />
|
||||||
|
public async Task RevokeAllForUser(Guid userId)
|
||||||
|
{
|
||||||
|
await WithDatabaseAsync(async connection => await connection.ExecuteAsync(
|
||||||
|
$"DELETE FROM {Table} WHERE userId = @pUserId", new
|
||||||
|
{
|
||||||
|
pUserId = userId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
48
Galaeth.DAL/Repositories/UserRepository.cs
Normal file
48
Galaeth.DAL/Repositories/UserRepository.cs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
using Dapper;
|
||||||
|
using Galaeth.Core.Entities;
|
||||||
|
using Galaeth.Core.Infrastructure;
|
||||||
|
using Galaeth.Core.RepositoryInterfaces;
|
||||||
|
using Injectio.Attributes;
|
||||||
|
|
||||||
|
namespace Galaeth.DAL.Repositories;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Galaeth.Core.RepositoryInterfaces.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="Galaeth.Core.RepositoryInterfaces.IUserRepository" />
|
||||||
|
public async Task<long> CountUsersAsync()
|
||||||
|
{
|
||||||
|
return await WithDatabaseAsync(async connection =>
|
||||||
|
await connection.QueryFirstAsync<long>($"SELECT COUNT(*) FROM {Table};"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Galaeth.Core.RepositoryInterfaces.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="Galaeth.Core.RepositoryInterfaces.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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
namespace Galaeth.Services.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration for the email domain blacklist service.
|
||||||
|
/// </summary>
|
||||||
|
public class EmailDomainBlacklistConfiguration
|
||||||
|
{
|
||||||
|
/// <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; }
|
||||||
|
}
|
22
Galaeth.Services/Configuration/InitialUserConfiguration.cs
Normal file
22
Galaeth.Services/Configuration/InitialUserConfiguration.cs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
namespace Galaeth.Services.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration for the first user created if none other exist.
|
||||||
|
/// </summary>
|
||||||
|
public class InitialUserConfiguration
|
||||||
|
{
|
||||||
|
/// <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; }
|
||||||
|
}
|
33
Galaeth.Services/Configuration/PwdHashConfiguration.cs
Normal file
33
Galaeth.Services/Configuration/PwdHashConfiguration.cs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
namespace Galaeth.Services.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How the password should be hashed.
|
||||||
|
/// Refer to https://argon2-cffi.readthedocs.io/en/stable/argon2.html.
|
||||||
|
/// </summary>
|
||||||
|
public class PwdHashConfiguration
|
||||||
|
{
|
||||||
|
/// <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; }
|
||||||
|
}
|
12
Galaeth.Services/Configuration/RegistrationConfiguration.cs
Normal file
12
Galaeth.Services/Configuration/RegistrationConfiguration.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
namespace Galaeth.Services.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User registration configuration.
|
||||||
|
/// </summary>
|
||||||
|
public class RegistrationConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether email verification should be required.
|
||||||
|
/// </summary>
|
||||||
|
public bool RequireEmailActivation { get; set; }
|
||||||
|
}
|
17
Galaeth.Services/Constants/ClaimIds.cs
Normal file
17
Galaeth.Services/Constants/ClaimIds.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
namespace Galaeth.Services.Constants;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Collection of claim ids.
|
||||||
|
/// </summary>
|
||||||
|
public static class ClaimIds
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// User Id claim.
|
||||||
|
/// </summary>
|
||||||
|
public const string UserId = "user-id";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User Role claim.
|
||||||
|
/// </summary>
|
||||||
|
public const string Role = "role";
|
||||||
|
}
|
37
Galaeth.Services/Constants/ServiceErrorCodes.cs
Normal file
37
Galaeth.Services/Constants/ServiceErrorCodes.cs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
namespace Galaeth.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";
|
||||||
|
}
|
37
Galaeth.Services/Dtos/AccessTokensDto.cs
Normal file
37
Galaeth.Services/Dtos/AccessTokensDto.cs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
namespace Galaeth.Services.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dto containing an access and refresh token for authentication.
|
||||||
|
/// </summary>
|
||||||
|
public class AccessTokensDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Access token.
|
||||||
|
/// </summary>
|
||||||
|
public string AccessToken { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refresh token.
|
||||||
|
/// </summary>
|
||||||
|
public string RefreshToken { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the access token expires.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime AccessTokenExpires { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the refresh token expires.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime RefreshTokenExpires { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the access token expires (UTC ticks).
|
||||||
|
/// </summary>
|
||||||
|
public long AccessTokenExpiresTicks { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the refresh token expires (UTC ticks).
|
||||||
|
/// </summary>
|
||||||
|
public long RefreshTokenExpiresTicks { get; set; }
|
||||||
|
}
|
20
Galaeth.Services/Dtos/AuthRequestBaseDto.cs
Normal file
20
Galaeth.Services/Dtos/AuthRequestBaseDto.cs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
using System.Net;
|
||||||
|
using MyCSharp.HttpUserAgentParser;
|
||||||
|
|
||||||
|
namespace Galaeth.Services.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A base for all authentication-related request dtos.
|
||||||
|
/// </summary>
|
||||||
|
public class AuthRequestBaseDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Agent's ip address.
|
||||||
|
/// </summary>
|
||||||
|
public IPAddress IpAddress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Agent's user agent information.
|
||||||
|
/// </summary>
|
||||||
|
public HttpUserAgentInformation UserAgent { get; set; }
|
||||||
|
}
|
20
Galaeth.Services/Dtos/AuthenticateUserDto.cs
Normal file
20
Galaeth.Services/Dtos/AuthenticateUserDto.cs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
using System.Net;
|
||||||
|
using MyCSharp.HttpUserAgentParser;
|
||||||
|
|
||||||
|
namespace Galaeth.Services.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authentication (login) request dto.
|
||||||
|
/// </summary>
|
||||||
|
public class AuthenticateUserDto : AuthRequestBaseDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Agent's username.
|
||||||
|
/// </summary>
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Agent's password.
|
||||||
|
/// </summary>
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
17
Galaeth.Services/Dtos/ChangePasswordDto.cs
Normal file
17
Galaeth.Services/Dtos/ChangePasswordDto.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
namespace Galaeth.Services.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A change of password request.
|
||||||
|
/// </summary>
|
||||||
|
public class ChangePasswordDto : AuthRequestBaseDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The user's old password.
|
||||||
|
/// </summary>
|
||||||
|
public string OldPassword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user's new password.
|
||||||
|
/// </summary>
|
||||||
|
public string NewPassword { get; set; }
|
||||||
|
}
|
34
Galaeth.Services/Dtos/CreateUserDto.cs
Normal file
34
Galaeth.Services/Dtos/CreateUserDto.cs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Galaeth.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; }
|
||||||
|
}
|
15
Galaeth.Services/Dtos/RefreshAuthenticationDto.cs
Normal file
15
Galaeth.Services/Dtos/RefreshAuthenticationDto.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
using System.Net;
|
||||||
|
using MyCSharp.HttpUserAgentParser;
|
||||||
|
|
||||||
|
namespace Galaeth.Services.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request the authentication of a user with a refresh token.
|
||||||
|
/// </summary>
|
||||||
|
public class RefreshAuthenticationDto : AuthRequestBaseDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The refresh token being used to re-authenticate a session.
|
||||||
|
/// </summary>
|
||||||
|
public string RefreshToken { get; set; }
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
using System.Net;
|
||||||
|
using Galaeth.Core.Exceptions;
|
||||||
|
using Galaeth.Services.Constants;
|
||||||
|
|
||||||
|
namespace Galaeth.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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
19
Galaeth.Services/Exceptions/InvalidCredentialsException.cs
Normal file
19
Galaeth.Services/Exceptions/InvalidCredentialsException.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
using System.Net;
|
||||||
|
using Galaeth.Core.Exceptions;
|
||||||
|
using Galaeth.Services.Constants;
|
||||||
|
|
||||||
|
namespace Galaeth.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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
19
Galaeth.Services/Exceptions/UnauthorizedException.cs
Normal file
19
Galaeth.Services/Exceptions/UnauthorizedException.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
using System.Net;
|
||||||
|
using Galaeth.Core.Constants;
|
||||||
|
using Galaeth.Core.Exceptions;
|
||||||
|
|
||||||
|
namespace Galaeth.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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
18
Galaeth.Services/Exceptions/UserBannedException.cs
Normal file
18
Galaeth.Services/Exceptions/UserBannedException.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using Galaeth.Core.Exceptions;
|
||||||
|
using Galaeth.Services.Constants;
|
||||||
|
|
||||||
|
namespace Galaeth.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", System.Net.HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
18
Galaeth.Services/Exceptions/UserNotActivatedException.cs
Normal file
18
Galaeth.Services/Exceptions/UserNotActivatedException.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using Galaeth.Core.Exceptions;
|
||||||
|
using Galaeth.Services.Constants;
|
||||||
|
|
||||||
|
namespace Galaeth.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.SuspendedUser, "Your account is pending activation. Please check your emails", System.Net.HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
18
Galaeth.Services/Exceptions/UserSuspendedException.cs
Normal file
18
Galaeth.Services/Exceptions/UserSuspendedException.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using Galaeth.Core.Exceptions;
|
||||||
|
using Galaeth.Services.Constants;
|
||||||
|
|
||||||
|
namespace Galaeth.Services.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when the logging in user is suspended.
|
||||||
|
/// </summary>
|
||||||
|
public class UserSuspendedException : ServiceException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="UserSuspendedException"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public UserSuspendedException()
|
||||||
|
: base(ServiceErrorCodes.SuspendedUser, "You are suspended from logging in", System.Net.HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
20
Galaeth.Services/Exceptions/UsernameTakenException.cs
Normal file
20
Galaeth.Services/Exceptions/UsernameTakenException.cs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
using System.Net;
|
||||||
|
using Galaeth.Core.Exceptions;
|
||||||
|
using Galaeth.Services.Constants;
|
||||||
|
|
||||||
|
namespace Galaeth.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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
24
Galaeth.Services/Galaeth.Services.csproj
Normal file
24
Galaeth.Services/Galaeth.Services.csproj
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<CodeAnalysisRuleSet>../Galaeth.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Galaeth.Core\Galaeth.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\Galaeth.DAL\Galaeth.DAL.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
30
Galaeth.Services/Interfaces/IAuthenticationService.cs
Normal file
30
Galaeth.Services/Interfaces/IAuthenticationService.cs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
using Galaeth.Services.Dtos;
|
||||||
|
|
||||||
|
namespace Galaeth.Services.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provide methods to authenticate new sessions.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAuthenticationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Authenticate a user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Instance of <see cref="AuthenticateUserDto"/>.</param>
|
||||||
|
/// <returns>Instance of <see cref="AccessTokensDto"/>.</returns>
|
||||||
|
Task<AccessTokensDto> AuthenticateUser(AuthenticateUserDto request);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authenticate a user with a refresh token.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Instance of <see cref="RefreshAuthenticationDto"/>.</param>
|
||||||
|
/// <returns>Instance of <see cref="AccessTokensDto"/>.</returns>
|
||||||
|
Task<AccessTokensDto> AuthenticateUser(RefreshAuthenticationDto request);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change user (requesting)'s password. Old password must be provided.
|
||||||
|
/// This action will remove all refresh tokens and generate a new token pair.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Instance of <see cref="ChangePasswordDto"/>.</param>
|
||||||
|
Task<AccessTokensDto> ChangeUserPassword(ChangePasswordDto request);
|
||||||
|
}
|
47
Galaeth.Services/Interfaces/ICryptographyService.cs
Normal file
47
Galaeth.Services/Interfaces/ICryptographyService.cs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
namespace Galaeth.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);
|
||||||
|
}
|
19
Galaeth.Services/Interfaces/IEmailDomainBlacklistService.cs
Normal file
19
Galaeth.Services/Interfaces/IEmailDomainBlacklistService.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
namespace Galaeth.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);
|
||||||
|
}
|
13
Galaeth.Services/Interfaces/IIdentityProvider.cs
Normal file
13
Galaeth.Services/Interfaces/IIdentityProvider.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
namespace Galaeth.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();
|
||||||
|
}
|
12
Galaeth.Services/Interfaces/IInitialUserService.cs
Normal file
12
Galaeth.Services/Interfaces/IInitialUserService.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
namespace Galaeth.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();
|
||||||
|
}
|
24
Galaeth.Services/Interfaces/IUserService.cs
Normal file
24
Galaeth.Services/Interfaces/IUserService.cs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
using Galaeth.Core.Dtos;
|
||||||
|
using Galaeth.Services.Dtos;
|
||||||
|
|
||||||
|
namespace Galaeth.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);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue