Add admin db project password seeding logic, extended init.sh (#113)

This commit is contained in:
Leendert de Borst
2024-07-22 17:14:21 +02:00
parent 25462e38bd
commit 6cb017af1c
21 changed files with 945 additions and 51 deletions

View File

@@ -45,6 +45,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Admin", "src\Ali
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Admin2", "src\AliasVault.Admin2\AliasVault.Admin2.csproj", "{F2CAE93E-94A7-4365-8E84-8D48CE8DD53F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InitializationCLI", "src\Utilities\InitializationCLI\InitializationCLI.csproj", "{857BCD0E-753F-437A-AF75-B995B4D9A5FE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -115,6 +117,10 @@ Global
{F2CAE93E-94A7-4365-8E84-8D48CE8DD53F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2CAE93E-94A7-4365-8E84-8D48CE8DD53F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2CAE93E-94A7-4365-8E84-8D48CE8DD53F}.Release|Any CPU.Build.0 = Release|Any CPU
{857BCD0E-753F-437A-AF75-B995B4D9A5FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{857BCD0E-753F-437A-AF75-B995B4D9A5FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{857BCD0E-753F-437A-AF75-B995B4D9A5FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{857BCD0E-753F-437A-AF75-B995B4D9A5FE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -131,6 +137,7 @@ Global
{A9C9A606-C87E-4298-AB32-09B1884D7487} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
{B095A174-E528-4D38-BEC1-D1D38B3B30C0} = {8A477241-B96C-4174-968D-D40CB77F1ECD}
{1C7C8DE9-5F2A-43DB-A25E-33319E80A509} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
{857BCD0E-753F-437A-AF75-B995B4D9A5FE} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FEE82475-C009-4762-8113-A6563D9DC49E}

View File

@@ -1,4 +1,16 @@
services:
admin:
image: aliasvault-admin
build:
context: .
dockerfile: src/AliasVault.Admin2/Dockerfile
ports:
- "8080:8082"
volumes:
- ./database:/database
restart: always
env_file:
- .env
client:
image: aliasvault-client
build:

166
init.sh
View File

@@ -13,6 +13,25 @@ NC='\033[0m' # No Color
ENV_FILE=".env"
ENV_EXAMPLE_FILE=".env.example"
# Define verbose flag
VERBOSE=false
# Function to parse command-line arguments
parse_args() {
while [ $# -gt 0 ]; do
case "$1" in
--verbose)
VERBOSE=true
;;
*)
printf "${RED}Unknown argument: $1${NC}\n"
exit 1
;;
esac
shift
done
}
# Function to generate a new 32-character JWT key
generate_jwt_key() {
dd if=/dev/urandom bs=1 count=32 2>/dev/null | base64 | head -c 32
@@ -87,6 +106,125 @@ set_smtp_tls_enabled() {
fi
}
# Function to generate a random admin password and store its hash in the .env file with progress indication
generate_admin_password() {
if grep -q "^ADMIN_PASSWORD_HASH=" ".env"; then
ADMIN_PASSWORD_GENERATED=$(grep "^ADMIN_PASSWORD_GENERATED=" ".env" | cut -d '=' -f2)
printf "${CYAN}> ADMIN_PASSWORD_HASH already exists in .env. Last generated at ${ADMIN_PASSWORD_GENERATED}.${NC}\n"
printf "\n"
read -p " Do you want to update the admin password? (yes/no): " confirm
if [[ "$confirm" != "yes" ]]; then
printf "${CYAN}> Admin password will not be changed.${NC}\n"
return 0
fi
# Remove existing entries
sed -i '' '/^ADMIN_PASSWORD_HASH=/d' .env
sed -i '' '/^ADMIN_PASSWORD_GENERATED=/d' .env
fi
ADMIN_PASSWORD=$(openssl rand -base64 12)
printf "\n"
printf "${BLUE} Building Docker image for password generation...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker build -t initcli -f src/Utilities/InitializationCLI/Dockerfile .
else
{
docker build -t initcli -f src/Utilities/InitializationCLI/Dockerfile . | while IFS= read -r line; do
printf "."
done
} > init_build_output.log 2>&1 &
BUILD_PID=$!
while kill -0 $BUILD_PID 2> /dev/null; do
printf "."
sleep 1
done
wait $BUILD_PID
BUILD_EXIT_CODE=$?
if [ $BUILD_EXIT_CODE -ne 0 ]; then
printf "\n${RED} Error occurred while building Docker image:${NC}\n"
cat init_build_output.log
return 1
fi
fi
printf "\n"
printf "${BLUE} Running Docker container to generate admin password hash...${NC}\n"
{
ADMIN_PASSWORD_HASH=$(docker run --rm initcli "$ADMIN_PASSWORD")
} &> init_run_output.log
if [ $? -ne 0 ]; then
printf "${RED} Error occurred while running Docker container:${NC}\n"
cat init_run_output.log
return 1
fi
CURRENT_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Append new entries
echo "ADMIN_PASSWORD_HASH=$ADMIN_PASSWORD_HASH" >> .env
echo "ADMIN_PASSWORD_GENERATED=$CURRENT_TIME" >> .env
printf "\n"
printf "${CYAN}> New admin password generated successfully.${NC}\n"
}
# Function to build and run the Docker Compose stack with muted output unless an error occurs, showing progress indication
build_and_run_docker_compose() {
printf "\n"
printf "${BLUE}Building Docker Compose stack...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker-compose build
else
{
docker-compose build | while IFS= read -r line; do
printf "."
done
} > compose_build_output.log 2>&1 &
BUILD_PID=$!
while kill -0 $BUILD_PID 2> /dev/null; do
printf "."
sleep 1
done
wait $BUILD_PID
BUILD_EXIT_CODE=$?
if [ $BUILD_EXIT_CODE -ne 0 ]; then
printf "\n${RED}Error occurred while building Docker Compose stack:${NC}\n"
cat compose_build_output.log
return 1
fi
fi
printf "\n"
printf "\n${BLUE}Starting Docker Compose stack...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker-compose up -d
else
{
docker-compose up -d | while IFS= read -r line; do
printf "."
done
} > compose_up_output.log 2>&1 &
UP_PID=$!
while kill -0 $UP_PID 2> /dev/null; do
printf "."
sleep 1
done
wait $UP_PID
UP_EXIT_CODE=$?
if [ $UP_EXIT_CODE -ne 0 ]; then
printf "\n${RED}Error occurred while starting Docker Compose stack:${NC}\n"
cat compose_up_output.log
return 1
fi
fi
printf "\n${GREEN}Docker Compose stack built and started successfully.${NC}\n"
}
# Function to print the CLI logo
print_logo() {
printf "${MAGENTA}\n"
@@ -104,15 +242,35 @@ print_logo() {
# Run the functions and print status
print_logo
printf "${BLUE}Initializing AliasVault...${NC}\n"
printf "${BLUE}+++ Initializing .env file...${NC}\n"
printf "\n"
create_env_file
populate_jwt_key
set_smtp_allowed_domains
set_smtp_tls_enabled
printf "${BLUE}Initialization complete.${NC}\n"
generate_admin_password
printf "\n${BLUE}+++ Finish initializing .env file...${NC}\n"
build_and_run_docker_compose
printf "${BLUE}If no errors are reported, the AliasVault Docker containers should have started successfully.${NC}\n"
printf "\n"
printf "To build the images and start the containers, run the following command:\n"
printf "${MAGENTA}=========================================================${NC}\n"
printf "\n"
printf "${CYAN}$ docker compose up -d --build --force-recreate${NC}\n"
printf "AliasVault is successfully initialized!\n"
printf "\n"
printf "You can now login to the admin panel:\n"
printf "\n"
if [ "$ADMIN_PASSWORD" != "" ]; then
printf "${CYAN}Admin Panel: http://localhost:8080/${NC}\n"
printf "${CYAN}Username: admin${NC}\n"
printf "${CYAN}Password: $ADMIN_PASSWORD${NC}\n"
printf "\n"
printf "(!) Caution: Make sure to backup the above credentials in a safe place, they won't be shown again!\n"
printf "\n"
else
printf "${CYAN}Admin Panel: http://localhost:8080/${NC}\n"
printf "${CYAN}Username: admin${NC}\n"
printf "${CYAN}Password: (Previously set.)${NC}\n"
printf "\n"
printf "\n"
fi

View File

@@ -8,6 +8,14 @@
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\net8.0\AliasVault.Admin2.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\net8.0\AliasVault.Admin2.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.6"/>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.6"/>
@@ -26,6 +34,9 @@
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="..\stylecop.json">
<Link>stylecop.json</Link>
</AdditionalFiles>
<AdditionalFiles Include="Auth\Pages\AccessDenied.razor" />
<AdditionalFiles Include="Auth\Pages\ConfirmEmail.razor" />
<AdditionalFiles Include="Auth\Pages\ConfirmEmailChange.razor" />
@@ -52,13 +63,10 @@
<AdditionalFiles Include="Main\Components\Alerts\GlobalNotificationDisplay.razor" />
<AdditionalFiles Include="Main\Components\Alerts\ServerValidationErrors.razor" />
<AdditionalFiles Include="Main\Pages\Account\Manage\ChangePassword.razor" />
<AdditionalFiles Include="Main\Pages\Account\Manage\DeletePersonalData.razor" />
<AdditionalFiles Include="Main\Pages\Account\Manage\Disable2fa.razor" />
<AdditionalFiles Include="Main\Pages\Account\Manage\Email.razor" />
<AdditionalFiles Include="Main\Pages\Account\Manage\EnableAuthenticator.razor" />
<AdditionalFiles Include="Main\Pages\Account\Manage\GenerateRecoveryCodes.razor" />
<AdditionalFiles Include="Main\Pages\Account\Manage\Index.razor" />
<AdditionalFiles Include="Main\Pages\Account\Manage\PersonalData.razor" />
<AdditionalFiles Include="Main\Pages\Account\Manage\ResetAuthenticator.razor" />
<AdditionalFiles Include="Main\Pages\Account\Manage\SetPassword.razor" />
<AdditionalFiles Include="Main\Pages\Account\Manage\TwoFactorAuthentication.razor" />

View File

@@ -11,9 +11,9 @@
<EditForm Model="Input" FormName="LoginForm" OnValidSubmit="LoginUser" class="mt-8 space-y-6">
<DataAnnotationsValidator/>
<div>
<label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
<InputTextField id="email" @bind-Value="Input.Email" placeholder="name@company.com" />
<ValidationMessage For="() => Input.Email"/>
<label asp-for="Input.UserName" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your username</label>
<InputTextField id="username" @bind-Value="Input.UserName" placeholder="username" />
<ValidationMessage For="() => Input.UserName"/>
</div>
<div>
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your password</label>
@@ -56,7 +56,7 @@
{
ServerValidationErrors.Clear();
var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
var result = await SignInManager.PasswordSignInAsync(Input.UserName, Input.Password, Input.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
Logger.LogInformation("User logged in.");
@@ -81,7 +81,7 @@
private sealed class InputModel
{
[Required] [EmailAddress] public string Email { get; set; } = "";
[Required] public string UserName { get; set; } = "";
[Required]
[DataType(DataType.Password)]

View File

@@ -17,9 +17,9 @@
<input type="hidden" name="Input.Code" value="@Input.Code"/>
<div class="form-floating mb-3">
<InputText @bind-Value="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com"/>
<label for="email" class="form-label">Email</label>
<ValidationMessage For="() => Input.Email" class="text-danger"/>
<InputText @bind-Value="Input.UserName" class="form-control" autocomplete="username" aria-required="true" placeholder="username"/>
<label for="username" class="form-label">Username</label>
<ValidationMessage For="() => Input.UserName" class="text-danger"/>
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please enter your password."/>
@@ -57,7 +57,7 @@
private async Task OnValidSubmitAsync()
{
var user = await UserManager.FindByEmailAsync(Input.Email);
var user = await UserManager.FindByNameAsync(Input.UserName);
if (user is null)
{
// Don't reveal that the user does not exist
@@ -75,7 +75,7 @@
private sealed class InputModel
{
[Required] [EmailAddress] public string Email { get; set; } = "";
[Required] public string UserName { get; set; } = "";
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]

View File

@@ -0,0 +1,26 @@
//-----------------------------------------------------------------------
// <copyright file="Config.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Admin2;
/// <summary>
/// Configuration class for the Admin project with values loaded from environment variables.
/// </summary>
public class Config
{
/// <summary>
/// Gets or sets the admin password hash which is generated by init.sh and will be set
/// as the default password for the admin user.
/// </summary>
public string AdminPasswordHash { get; set; } = "false";
/// <summary>
/// Gets or sets the last time the password was changed. This is used to check if the
/// password hash generated by init.sh should replace the current password hash if user already exists.
/// </summary>
public DateTime LastPasswordChanged { get; set; } = DateTime.MinValue;
}

View File

@@ -1,23 +1,33 @@
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
# Use the official .NET 8 SDK image to build the app
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["src/AliasVault.Admin2/AliasVault.Admin2.csproj", "src/AliasVault.Admin2/"]
RUN dotnet restore "src/AliasVault.Admin2/AliasVault.Admin2.csproj"
COPY . .
WORKDIR "/src/src/AliasVault.Admin2"
RUN dotnet build "AliasVault.Admin2.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "AliasVault.Admin2.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
# Copy the solution file to the /src directory in the container
COPY aliasvault.sln ./
# Copy the project file to the /src/AliasVault directory in the container
COPY src/AliasVault.Admin2/AliasVault.Admin2.csproj ./AliasVault.Admin2/
# Restore dependencies for the AliasVault project
RUN dotnet restore "./AliasVault.Admin2/AliasVault.Admin2.csproj" --verbosity detailed
# Copy the rest of the application code to the /src directory in the container
COPY src/. ./
# Publish the application to the /app/publish directory in the container
WORKDIR /src/AliasVault.Admin2
RUN dotnet publish -c Release -o /app --verbosity detailed
# Use the official ASP.NET Core runtime image to run the app
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app/AliasVault.Admin2
# Copy the published output from the build stage to the runtime stage
COPY --from=build /app ./
# Expose the port the app runs on
EXPOSE 8082
ENV ASPNETCORE_URLS=http://+:8082
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "AliasVault.Admin2.dll"]

View File

@@ -10,7 +10,6 @@
<div class="max-w-2xl mx-auto">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Change password</h3>
<StatusMessage Message="@message" class="mb-6"/>
<EditForm Model="Input" FormName="change-password" OnValidSubmit="OnValidSubmitAsync" method="post" class="space-y-6">
<DataAnnotationsValidator/>
<ValidationSummary class="text-red-600 dark:text-red-400" role="alert"/>
@@ -38,7 +37,6 @@
</div>
@code {
private string? message;
private bool hasPassword;
[CascadingParameter] private HttpContext HttpContext { get; set; } = default!;
@@ -57,9 +55,12 @@
private async Task OnValidSubmitAsync()
{
var changePasswordResult = await UserManager.ChangePasswordAsync(UserService.User(), Input.OldPassword, Input.NewPassword);
var user = UserService.User();
user.LastPasswordChanged = DateTime.UtcNow;
await UserService.UpdateUserAsync(user);
if (!changePasswordResult.Succeeded)
{
message = $"Error: {string.Join(",", changePasswordResult.Errors.Select(error => error.Description))}";
GlobalNotificationService.AddErrorMessage($"Error: {string.Join(",", changePasswordResult.Errors.Select(error => error.Description))}");
return;
}

View File

@@ -1,5 +1,7 @@
using System.Data.Common;
using System.Globalization;
using AliasServerDb;
using AliasVault.Admin2;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
@@ -10,6 +12,18 @@ using Microsoft.Data.Sqlite;
var builder = WebApplication.CreateBuilder(args);
// Create global config object, get values from environment variables.
Config config = new Config();
var adminPasswordHash = Environment.GetEnvironmentVariable("ADMIN_PASSWORD_HASH")
?? throw new KeyNotFoundException("ADMIN_PASSWORD_HASH environment variable is not set.");
config.AdminPasswordHash = adminPasswordHash;
var lastPasswordChanged = Environment.GetEnvironmentVariable("ADMIN_PASSWORD_GENERATED")
?? throw new KeyNotFoundException("ADMIN_PASSWORD_GENERATED environment variable is not set.");
config.LastPasswordChanged = DateTime.ParseExact(lastPasswordChanged, "yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
builder.Services.AddSingleton(config);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
@@ -64,6 +78,7 @@ builder.Services.AddIdentityCore<AdminUser>(options =>
options.Password.RequiredLength = 8;
options.Password.RequiredUniqueChars = 0;
options.SignIn.RequireConfirmedAccount = false;
options.User.RequireUniqueEmail = false;
})
.AddRoles<AdminRole>()
.AddEntityFrameworkStores<AliasServerDbContext>()
@@ -92,4 +107,10 @@ app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
using (var scope = app.Services.CreateScope())
{
await StartupTasks.CreateRolesIfNotExist(scope.ServiceProvider);
await StartupTasks.SetAdminUser(scope.ServiceProvider);
}
app.Run();

View File

@@ -15,7 +15,9 @@
"launchBrowser": true,
"applicationUrl": "http://localhost:5216",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
"ASPNETCORE_ENVIRONMENT": "Development",
"ADMIN_PASSWORD_HASH": "AQAAAAIAAYagAAAAEKWfKfa2gh9Z72vjAlnNP1xlME7FsunRznzyrfqFte40FToufRwa3kX8wwDwnEXZag==",
"ADMIN_PASSWORD_GENERATED": "2024-07-22T16:36:02Z"
}
},
"https": {

View File

@@ -187,9 +187,9 @@ public class UserService
/// Update user.
/// </summary>
/// <param name="user">User object.</param>
/// <param name="newPassword">New password for the user.</param>
/// <param name="newPassword">Optional parameter for new password for the user.</param>
/// <returns>List of errors if any.</returns>
public async Task<List<string>> UpdateUserAsync(AdminUser user, string newPassword)
public async Task<List<string>> UpdateUserAsync(AdminUser user, string newPassword = "")
{
var errors = await ValidateUser(user, newPassword, isUpdate: true);
if (errors.Count > 0)

View File

@@ -0,0 +1,70 @@
namespace AliasVault.Admin2;
using AliasServerDb;
using Microsoft.AspNetCore.Identity;
/// <summary>
/// Startup tasks that should be run when the application starts.
/// </summary>
public static class StartupTasks
{
/// <summary>
/// Creates the roles if they do not exist.
/// </summary>
/// <param name="serviceProvider">IServiceProvider instance.</param>
/// <returns>Task.</returns>
public static async Task CreateRolesIfNotExist(IServiceProvider serviceProvider)
{
var roleManager = serviceProvider.GetRequiredService<RoleManager<AdminRole>>();
const string adminRole = "Admin";
if (!await roleManager.RoleExistsAsync(adminRole))
{
await roleManager.CreateAsync(new AdminRole(adminRole));
}
}
/// <summary>
/// Creates the admin user if it does not exist.
/// </summary>
/// <param name="serviceProvider"></param>
/// <returns>Async Task.</returns>
public static async Task SetAdminUser(IServiceProvider serviceProvider)
{
using var scope = serviceProvider.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<AdminUser>>();
var adminUser = await userManager.FindByNameAsync("admin");
var config = serviceProvider.GetRequiredService<Config>();
if (adminUser == null)
{
var adminPasswordHash = config.AdminPasswordHash;
adminUser = new AdminUser();
adminUser.UserName = "admin";
await userManager.CreateAsync(adminUser);
adminUser.PasswordHash = adminPasswordHash;
await userManager.UpdateAsync(adminUser);
Console.WriteLine("Admin user created.");
}
else
{
// Check if the password hash is correct, if not, update it to the .env value.
if (adminUser.PasswordHash != config.AdminPasswordHash)
{
if (config.LastPasswordChanged > adminUser.LastPasswordChanged)
{
// The password has been changed in the .env file, update the user's password hash.
adminUser.PasswordHash = config.AdminPasswordHash;
adminUser.LastPasswordChanged = DateTime.UtcNow;
await userManager.UpdateAsync(adminUser);
Console.WriteLine("Admin password hash updated.");
}
}
}
}
}

View File

@@ -14,4 +14,8 @@ using Microsoft.AspNetCore.Identity;
/// </summary>
public class AdminUser : IdentityUser
{
/// <summary>
/// Gets or sets the last time the password was changed.
/// </summary>
public DateTime? LastPasswordChanged { get; set; }
}

View File

@@ -19,14 +19,6 @@ using Microsoft.EntityFrameworkCore;
[Index(nameof(PushNotificationSent))]
public class Email
{
/// <summary>
/// Initializes a new instance of the <see cref="Email"/> class.
/// </summary>
public Email()
{
Attachments = new HashSet<EmailAttachment>();
}
/// <summary>
/// Gets or sets the ID of the email.
/// </summary>
@@ -110,5 +102,5 @@ public class Email
/// <summary>
/// Gets or sets the collection of email attachments.
/// </summary>
public virtual ICollection<EmailAttachment> Attachments { get; set; }
public virtual ICollection<EmailAttachment> Attachments { get; set; } = [];
}

View File

@@ -0,0 +1,487 @@
// <auto-generated />
using System;
using AliasServerDb;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace AliasServerDb.Migrations
{
[DbContext(typeof(AliasServerDbContext))]
[Migration("20240722144205_AddAdminUserPasswordSetDate")]
partial class AddAdminUserPasswordSetDate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.6")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true);
modelBuilder.Entity("AliasServerDb.AdminRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("AdminRoles");
});
modelBuilder.Entity("AliasServerDb.AdminUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastPasswordChanged")
.HasColumnType("TEXT");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("AdminUsers");
});
modelBuilder.Entity("AliasServerDb.AliasVaultRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("AliasVaultRoles");
});
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("Salt")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasColumnType("TEXT");
b.Property<string>("Verifier")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("AliasVaultUsers");
});
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("DeviceIdentifier")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("ExpireDate")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AliasVaultUserRefreshTokens");
});
modelBuilder.Entity("AliasServerDb.Email", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Date")
.HasColumnType("TEXT");
b.Property<DateTime>("DateSystem")
.HasColumnType("TEXT");
b.Property<string>("From")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("FromDomain")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("FromLocal")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("MessageHtml")
.HasColumnType("TEXT");
b.Property<string>("MessagePlain")
.HasColumnType("TEXT");
b.Property<string>("MessagePreview")
.HasColumnType("TEXT");
b.Property<string>("MessageSource")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("PushNotificationSent")
.HasColumnType("INTEGER");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("To")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ToDomain")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ToLocal")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("Visible")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Date");
b.HasIndex("DateSystem");
b.HasIndex("PushNotificationSent");
b.HasIndex("ToLocal");
b.HasIndex("Visible");
b.ToTable("Emails");
});
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<byte[]>("Bytes")
.IsRequired()
.HasColumnType("BLOB");
b.Property<DateTime>("Date")
.HasColumnType("TEXT");
b.Property<int>("EmailId")
.HasColumnType("INTEGER");
b.Property<string>("Filename")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Filesize")
.HasColumnType("INTEGER");
b.Property<string>("MimeType")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("EmailId");
b.ToTable("EmailAttachments");
});
modelBuilder.Entity("AliasServerDb.Vault", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("VaultBlob")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Vaults");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("RoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("UserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.ToTable("UserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.ToTable("UserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("UserTokens", (string)null);
});
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
{
b.HasOne("AliasServerDb.Email", "Email")
.WithMany("Attachments")
.HasForeignKey("EmailId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Email");
});
modelBuilder.Entity("AliasServerDb.Vault", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.Email", b =>
{
b.Navigation("Attachments");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AliasServerDb.Migrations
{
/// <inheritdoc />
public partial class AddAdminUserPasswordSetDate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "LastPasswordChanged",
table: "AdminUsers",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LastPasswordChanged",
table: "AdminUsers");
}
}
}

View File

@@ -57,6 +57,9 @@ namespace AliasServerDb.Migrations
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastPasswordChanged")
.HasColumnType("TEXT");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");

View File

@@ -0,0 +1,26 @@
FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
# Copy csproj files and restore as distinct layers
COPY ["src/Utilities/InitializationCLI/InitializationCLI.csproj", "src/Utilities/InitializationCLI/"]
COPY ["src/Databases/AliasServerDb/AliasServerDb.csproj", "src/Databases/AliasServerDb/"]
RUN dotnet restore "src/Utilities/InitializationCLI/InitializationCLI.csproj"
# Copy the entire source code
COPY . .
# Build the project
RUN dotnet build "src/Utilities/InitializationCLI/InitializationCLI.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "src/Utilities/InitializationCLI/InitializationCLI.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "InitializationCLI.dll"]

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Content Include="..\..\..\.dockerignore">
<Link>.dockerignore</Link>
<DependentUpon>Dockerfile</DependentUpon>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Databases\AliasServerDb\AliasServerDb.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,17 @@
// See https://aka.ms/new-console-template for more information
using System;
using AliasServerDb;
using Microsoft.AspNetCore.Identity;
if (args.Length == 0)
{
Console.WriteLine("Please provide a password as an argument.");
return;
}
var password = args[0];
var hasher = new PasswordHasher<IdentityUser>();
var user = new AdminUser();
var hashedPassword = hasher.HashPassword(user, password);
Console.WriteLine($"{hashedPassword}");