mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-04-05 18:09:04 +00:00
Begin work on user confirmation workflow
This commit is contained in:
@@ -35,7 +35,9 @@ DB_PASSWORD=YourStrong!Passw0rd
|
|||||||
# JWT Secret for signing tokens
|
# JWT Secret for signing tokens
|
||||||
# IMPORTANT: Generate a secure secret (minimum 32 characters)
|
# IMPORTANT: Generate a secure secret (minimum 32 characters)
|
||||||
# Command: openssl rand -base64 32
|
# Command: openssl rand -base64 32
|
||||||
JWT_SECRET=128490218jfklsdajfdsa90f8sd0fid0safasr31jl2k1j4AFSDR
|
ACCESS_TOKEN_SECRET=your-secure-jwt-secret-key
|
||||||
|
REFRESH_TOKEN_SECRET=your-secure-jwt-refresh-secret-key
|
||||||
|
CONFIRMATION_TOKEN_SECRET=your-secure-jwt-confirmation-secret-key
|
||||||
|
|
||||||
|
|
||||||
# ======================
|
# ======================
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- sqlserverdata-dev:/var/opt/mssql
|
- sqlserverdata-dev:/var/opt/mssql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1"]
|
test: [ "CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1" ]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 12
|
retries: 12
|
||||||
@@ -91,7 +91,9 @@ services:
|
|||||||
DB_NAME: "${DB_NAME}"
|
DB_NAME: "${DB_NAME}"
|
||||||
DB_USER: "${DB_USER}"
|
DB_USER: "${DB_USER}"
|
||||||
DB_PASSWORD: "${DB_PASSWORD}"
|
DB_PASSWORD: "${DB_PASSWORD}"
|
||||||
JWT_SECRET: "${JWT_SECRET}"
|
ACCESS_TOKEN_SECRET: "${ACCESS_TOKEN_SECRET}"
|
||||||
|
REFRESH_TOKEN_SECRET: "${REFRESH_TOKEN_SECRET}"
|
||||||
|
CONFIRMATION_TOKEN_SECRET: "${CONFIRMATION_TOKEN_SECRET}"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- devnet
|
- devnet
|
||||||
@@ -99,18 +101,18 @@ services:
|
|||||||
- nuget-cache-dev:/root/.nuget/packages
|
- nuget-cache-dev:/root/.nuget/packages
|
||||||
|
|
||||||
mailpit:
|
mailpit:
|
||||||
image: axllent/mailpit:latest
|
image: axllent/mailpit:latest
|
||||||
container_name: dev-env-mailpit
|
container_name: dev-env-mailpit
|
||||||
ports:
|
ports:
|
||||||
- "8025:8025" # Web UI
|
- "8025:8025" # Web UI
|
||||||
- "1025:1025" # SMTP server
|
- "1025:1025" # SMTP server
|
||||||
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
MP_SMTP_AUTH_ACCEPT_ANY: 1
|
MP_SMTP_AUTH_ACCEPT_ANY: 1
|
||||||
MP_SMTP_AUTH_ALLOW_INSECURE: 1
|
MP_SMTP_AUTH_ALLOW_INSECURE: 1
|
||||||
networks:
|
networks:
|
||||||
- devnet
|
- devnet
|
||||||
volumes:
|
volumes:
|
||||||
sqlserverdata-dev:
|
sqlserverdata-dev:
|
||||||
driver: local
|
driver: local
|
||||||
|
|||||||
91
docker-compose.min.yaml
Normal file
91
docker-compose.min.yaml
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
services:
|
||||||
|
sqlserver:
|
||||||
|
env_file: ".env.local"
|
||||||
|
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||||
|
platform: linux/amd64
|
||||||
|
container_name: dev-env-sqlserver
|
||||||
|
environment:
|
||||||
|
ACCEPT_EULA: "Y"
|
||||||
|
SA_PASSWORD: "${DB_PASSWORD}"
|
||||||
|
MSSQL_PID: "Express"
|
||||||
|
ports:
|
||||||
|
- "1433:1433"
|
||||||
|
volumes:
|
||||||
|
- sqlserverdata-dev:/var/opt/mssql
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
start_period: 30s
|
||||||
|
networks:
|
||||||
|
- devnet
|
||||||
|
database.migrations:
|
||||||
|
env_file: ".env.local"
|
||||||
|
image: database.migrations
|
||||||
|
container_name: dev-env-database-migrations
|
||||||
|
depends_on:
|
||||||
|
sqlserver:
|
||||||
|
condition: service_healthy
|
||||||
|
build:
|
||||||
|
context: ./src/Core/Database
|
||||||
|
dockerfile: Database.Migrations/Dockerfile
|
||||||
|
args:
|
||||||
|
BUILD_CONFIGURATION: Release
|
||||||
|
APP_UID: 1000
|
||||||
|
environment:
|
||||||
|
DOTNET_RUNNING_IN_CONTAINER: "true"
|
||||||
|
DB_SERVER: "${DB_SERVER}"
|
||||||
|
DB_NAME: "${DB_NAME}"
|
||||||
|
DB_USER: "${DB_USER}"
|
||||||
|
DB_PASSWORD: "${DB_PASSWORD}"
|
||||||
|
CLEAR_DATABASE: "true"
|
||||||
|
restart: "no"
|
||||||
|
networks:
|
||||||
|
- devnet
|
||||||
|
|
||||||
|
mailpit:
|
||||||
|
image: axllent/mailpit:latest
|
||||||
|
container_name: dev-env-mailpit
|
||||||
|
ports:
|
||||||
|
- "8025:8025" # Web UI
|
||||||
|
- "1025:1025" # SMTP server
|
||||||
|
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MP_SMTP_AUTH_ACCEPT_ANY: 1
|
||||||
|
MP_SMTP_AUTH_ALLOW_INSECURE: 1
|
||||||
|
networks:
|
||||||
|
- devnet
|
||||||
|
|
||||||
|
database.seed:
|
||||||
|
env_file: ".env.local"
|
||||||
|
image: database.seed
|
||||||
|
container_name: dev-env-database-seed
|
||||||
|
depends_on:
|
||||||
|
database.migrations:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
build:
|
||||||
|
context: ./src/Core
|
||||||
|
dockerfile: Database/Database.Seed/Dockerfile
|
||||||
|
args:
|
||||||
|
BUILD_CONFIGURATION: Release
|
||||||
|
APP_UID: 1000
|
||||||
|
environment:
|
||||||
|
DOTNET_RUNNING_IN_CONTAINER: "true"
|
||||||
|
DB_SERVER: "${DB_SERVER}"
|
||||||
|
DB_NAME: "${DB_NAME}"
|
||||||
|
DB_USER: "${DB_USER}"
|
||||||
|
DB_PASSWORD: "${DB_PASSWORD}"
|
||||||
|
restart: "no"
|
||||||
|
networks:
|
||||||
|
- devnet
|
||||||
|
volumes:
|
||||||
|
sqlserverdata-dev:
|
||||||
|
driver: local
|
||||||
|
nuget-cache-dev:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
devnet:
|
||||||
|
driver: bridge
|
||||||
@@ -12,7 +12,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- sqlserverdata-test:/var/opt/mssql
|
- sqlserverdata-test:/var/opt/mssql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1"]
|
test: [ "CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1" ]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 12
|
retries: 12
|
||||||
@@ -85,7 +85,9 @@ services:
|
|||||||
DB_NAME: "${DB_NAME}"
|
DB_NAME: "${DB_NAME}"
|
||||||
DB_USER: "${DB_USER}"
|
DB_USER: "${DB_USER}"
|
||||||
DB_PASSWORD: "${DB_PASSWORD}"
|
DB_PASSWORD: "${DB_PASSWORD}"
|
||||||
JWT_SECRET: "${JWT_SECRET}"
|
ACCESS_TOKEN_SECRET: "${ACCESS_TOKEN_SECRET}"
|
||||||
|
REFRESH_TOKEN_SECRET: "${REFRESH_TOKEN_SECRET}"
|
||||||
|
CONFIRMATION_TOKEN_SECRET: "${CONFIRMATION_TOKEN_SECRET}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./test-results:/app/test-results
|
- ./test-results:/app/test-results
|
||||||
restart: "no"
|
restart: "no"
|
||||||
|
|||||||
@@ -17,3 +17,7 @@ public record RegistrationPayload(
|
|||||||
string AccessToken,
|
string AccessToken,
|
||||||
bool ConfirmationEmailSent
|
bool ConfirmationEmailSent
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public record ConfirmationPayload(
|
||||||
|
Guid UserAccountId,
|
||||||
|
DateTime ConfirmedDate);
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ namespace API.Core.Controllers
|
|||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class AuthController(IRegisterService register, ILoginService login)
|
public class AuthController(
|
||||||
|
IRegisterService registerService,
|
||||||
|
ILoginService loginService,
|
||||||
|
IConfirmationService confirmationService)
|
||||||
: ControllerBase
|
: ControllerBase
|
||||||
{
|
{
|
||||||
[HttpPost("register")]
|
[HttpPost("register")]
|
||||||
@@ -16,7 +19,7 @@ namespace API.Core.Controllers
|
|||||||
[FromBody] RegisterRequest req
|
[FromBody] RegisterRequest req
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var rtn = await register.RegisterAsync(
|
var rtn = await registerService.RegisterAsync(
|
||||||
new UserAccount
|
new UserAccount
|
||||||
{
|
{
|
||||||
UserAccountId = Guid.Empty,
|
UserAccountId = Guid.Empty,
|
||||||
@@ -46,7 +49,7 @@ namespace API.Core.Controllers
|
|||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
public async Task<ActionResult> Login([FromBody] LoginRequest req)
|
public async Task<ActionResult> Login([FromBody] LoginRequest req)
|
||||||
{
|
{
|
||||||
var rtn = await login.LoginAsync(req.Username, req.Password);
|
var rtn = await loginService.LoginAsync(req.Username, req.Password);
|
||||||
|
|
||||||
return Ok(
|
return Ok(
|
||||||
new ResponseBody<LoginPayload>
|
new ResponseBody<LoginPayload>
|
||||||
@@ -61,5 +64,18 @@ namespace API.Core.Controllers
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("confirm")]
|
||||||
|
public async Task<ActionResult> Confirm([FromQuery] string token)
|
||||||
|
{
|
||||||
|
var rtn = await confirmationService.ConfirmUserAsync(token);
|
||||||
|
return Ok(new ResponseBody<ConfirmationPayload>
|
||||||
|
{
|
||||||
|
Message = "User with ID " + rtn.userId + " is confirmed.",
|
||||||
|
Payload = new ConfirmationPayload(
|
||||||
|
rtn.userId, rtn.confirmedAt
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,6 +64,7 @@ builder.Services.AddScoped<IPasswordInfrastructure, Argon2Infrastructure>();
|
|||||||
builder.Services.AddScoped<IEmailProvider, SmtpEmailProvider>();
|
builder.Services.AddScoped<IEmailProvider, SmtpEmailProvider>();
|
||||||
builder.Services.AddScoped<IEmailTemplateProvider, EmailTemplateProvider>();
|
builder.Services.AddScoped<IEmailTemplateProvider, EmailTemplateProvider>();
|
||||||
builder.Services.AddScoped<IEmailService, EmailService>();
|
builder.Services.AddScoped<IEmailService, EmailService>();
|
||||||
|
builder.Services.AddScoped<IConfirmationService, ConfirmationService>();
|
||||||
|
|
||||||
// Register the exception filter
|
// Register the exception filter
|
||||||
builder.Services.AddScoped<GlobalExceptionFilter>();
|
builder.Services.AddScoped<GlobalExceptionFilter>();
|
||||||
|
|||||||
@@ -2,5 +2,10 @@ namespace Infrastructure.Jwt;
|
|||||||
|
|
||||||
public interface ITokenInfrastructure
|
public interface ITokenInfrastructure
|
||||||
{
|
{
|
||||||
string GenerateJwt(Guid userId, string username, DateTime expiry);
|
string GenerateJwt(
|
||||||
|
Guid userId,
|
||||||
|
string username,
|
||||||
|
DateTime expiry,
|
||||||
|
string secret
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,17 @@ namespace Infrastructure.Jwt;
|
|||||||
|
|
||||||
public class JwtInfrastructure : ITokenInfrastructure
|
public class JwtInfrastructure : ITokenInfrastructure
|
||||||
{
|
{
|
||||||
private readonly string? _secret = Environment.GetEnvironmentVariable(
|
public string GenerateJwt(
|
||||||
"JWT_SECRET"
|
Guid userId,
|
||||||
);
|
string username,
|
||||||
|
DateTime expiry,
|
||||||
public string GenerateJwt(Guid userId, string username, DateTime expiry)
|
string secret
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var handler = new JsonWebTokenHandler();
|
var handler = new JsonWebTokenHandler();
|
||||||
|
|
||||||
var key = Encoding.UTF8.GetBytes(
|
var key = Encoding.UTF8.GetBytes(
|
||||||
_secret ?? throw new InvalidOperationException("secret not set")
|
secret ?? throw new InvalidOperationException("secret not set")
|
||||||
);
|
);
|
||||||
|
|
||||||
// Base claims (always present)
|
// Base claims (always present)
|
||||||
|
|||||||
21
src/Core/Service/Service.Auth/IConfirmationService.cs
Normal file
21
src/Core/Service/Service.Auth/IConfirmationService.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Runtime.InteropServices.JavaScript;
|
||||||
|
using Infrastructure.Repository.Auth;
|
||||||
|
|
||||||
|
namespace Service.Auth;
|
||||||
|
|
||||||
|
public record ConfirmationServiceReturn(DateTime confirmedAt, Guid userId);
|
||||||
|
|
||||||
|
public interface IConfirmationService
|
||||||
|
{
|
||||||
|
Task<ConfirmationServiceReturn> ConfirmUserAsync(string confirmationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class ConfirmationService(IAuthRepository authRepository) : IConfirmationService
|
||||||
|
{
|
||||||
|
|
||||||
|
public async Task<ConfirmationServiceReturn> ConfirmUserAsync(string confirmationToken)
|
||||||
|
{
|
||||||
|
return new ConfirmationServiceReturn(DateTime.Now, Guid.NewGuid());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,11 @@ using Domain.Entities;
|
|||||||
|
|
||||||
namespace Service.Auth;
|
namespace Service.Auth;
|
||||||
|
|
||||||
|
public record LoginServiceReturn(
|
||||||
|
UserAccount UserAccount,
|
||||||
|
string RefreshToken,
|
||||||
|
string AccessToken
|
||||||
|
);
|
||||||
public interface ILoginService
|
public interface ILoginService
|
||||||
{
|
{
|
||||||
Task<LoginServiceReturn> LoginAsync(string username, string password);
|
Task<LoginServiceReturn> LoginAsync(string username, string password);
|
||||||
|
|||||||
@@ -36,4 +36,4 @@ public interface IRegisterService
|
|||||||
UserAccount userAccount,
|
UserAccount userAccount,
|
||||||
string password
|
string password
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -7,28 +7,73 @@ public interface ITokenService
|
|||||||
{
|
{
|
||||||
public string GenerateAccessToken(UserAccount user);
|
public string GenerateAccessToken(UserAccount user);
|
||||||
public string GenerateRefreshToken(UserAccount user);
|
public string GenerateRefreshToken(UserAccount user);
|
||||||
|
public string GenerateConfirmationToken(UserAccount user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TokenServiceExpirationHours
|
||||||
|
{
|
||||||
|
public const double AccessTokenHours = 1;
|
||||||
|
public const double RefreshTokenHours = 504; // 21 days
|
||||||
|
public const double ConfirmationTokenHours = 0.5; // 30 minutes
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TokenService(ITokenInfrastructure tokenInfrastructure)
|
public class TokenService(ITokenInfrastructure tokenInfrastructure)
|
||||||
: ITokenService
|
: ITokenService
|
||||||
{
|
{
|
||||||
|
private readonly string _accessTokenSecret =
|
||||||
|
Environment.GetEnvironmentVariable("ACCESS_TOKEN_SECRET")
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"ACCESS_TOKEN_SECRET environment variable is not set"
|
||||||
|
);
|
||||||
|
|
||||||
|
private readonly string _refreshTokenSecret =
|
||||||
|
Environment.GetEnvironmentVariable("REFRESH_TOKEN_SECRET")
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"REFRESH_TOKEN_SECRET environment variable is not set"
|
||||||
|
);
|
||||||
|
|
||||||
|
private readonly string _confirmationTokenSecret =
|
||||||
|
Environment.GetEnvironmentVariable("CONFIRMATION_TOKEN_SECRET")
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"CONFIRMATION_TOKEN_SECRET environment variable is not set"
|
||||||
|
);
|
||||||
|
|
||||||
public string GenerateAccessToken(UserAccount userAccount)
|
public string GenerateAccessToken(UserAccount userAccount)
|
||||||
{
|
{
|
||||||
var jwtExpiresAt = DateTime.UtcNow.AddHours(1);
|
var jwtExpiresAt = DateTime.UtcNow.AddHours(
|
||||||
|
TokenServiceExpirationHours.AccessTokenHours
|
||||||
|
);
|
||||||
return tokenInfrastructure.GenerateJwt(
|
return tokenInfrastructure.GenerateJwt(
|
||||||
userAccount.UserAccountId,
|
userAccount.UserAccountId,
|
||||||
userAccount.Username,
|
userAccount.Username,
|
||||||
jwtExpiresAt
|
jwtExpiresAt,
|
||||||
|
_accessTokenSecret
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GenerateRefreshToken(UserAccount userAccount)
|
public string GenerateRefreshToken(UserAccount userAccount)
|
||||||
{
|
{
|
||||||
var jwtExpiresAt = DateTime.UtcNow.AddDays(21);
|
var jwtExpiresAt = DateTime.UtcNow.AddHours(
|
||||||
|
TokenServiceExpirationHours.RefreshTokenHours
|
||||||
|
);
|
||||||
return tokenInfrastructure.GenerateJwt(
|
return tokenInfrastructure.GenerateJwt(
|
||||||
userAccount.UserAccountId,
|
userAccount.UserAccountId,
|
||||||
userAccount.Username,
|
userAccount.Username,
|
||||||
jwtExpiresAt
|
jwtExpiresAt,
|
||||||
|
_refreshTokenSecret
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GenerateConfirmationToken(UserAccount userAccount)
|
||||||
|
{
|
||||||
|
var jwtExpiresAt = DateTime.UtcNow.AddHours(
|
||||||
|
TokenServiceExpirationHours.ConfirmationTokenHours
|
||||||
|
);
|
||||||
|
return tokenInfrastructure.GenerateJwt(
|
||||||
|
userAccount.UserAccountId,
|
||||||
|
userAccount.Username,
|
||||||
|
jwtExpiresAt,
|
||||||
|
_confirmationTokenSecret
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,6 @@ using Infrastructure.Repository.Auth;
|
|||||||
|
|
||||||
namespace Service.Auth;
|
namespace Service.Auth;
|
||||||
|
|
||||||
public record LoginServiceReturn(
|
|
||||||
UserAccount UserAccount,
|
|
||||||
string RefreshToken,
|
|
||||||
string AccessToken
|
|
||||||
);
|
|
||||||
|
|
||||||
public class LoginService(
|
public class LoginService(
|
||||||
IAuthRepository authRepo,
|
IAuthRepository authRepo,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ public class RegisterService(
|
|||||||
|
|
||||||
var accessToken = tokenService.GenerateAccessToken(createdUser);
|
var accessToken = tokenService.GenerateAccessToken(createdUser);
|
||||||
var refreshToken = tokenService.GenerateRefreshToken(createdUser);
|
var refreshToken = tokenService.GenerateRefreshToken(createdUser);
|
||||||
|
var confirmationToken = tokenService.GenerateConfirmationToken(createdUser);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
string.IsNullOrEmpty(accessToken)
|
string.IsNullOrEmpty(accessToken)
|
||||||
@@ -67,14 +68,15 @@ public class RegisterService(
|
|||||||
{
|
{
|
||||||
// send confirmation email
|
// send confirmation email
|
||||||
await emailService.SendRegistrationEmailAsync(
|
await emailService.SendRegistrationEmailAsync(
|
||||||
createdUser,
|
createdUser, confirmationToken
|
||||||
"some-confirmation-token"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
emailSent = true;
|
emailSent = true;
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
await Console.Error.WriteLineAsync(ex.Message);
|
||||||
|
Console.WriteLine("Could not send email.");
|
||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,4 +87,4 @@ public class RegisterService(
|
|||||||
emailSent
|
emailSent
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user