Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
.sonarlint/
.sonarqube/
.vscode/
.venv
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Complete the `appsettings.json` file with the following content, replacing place
"Port": 587,
"Username": "<your-email (optional)>",
"Password": "<your-email-password (optional)>",
"From": "<your-email (optional)>",
"From": "<your-email (optional)>"
},
"Jwt": {
"Key": "<your-random-key>",
Expand Down Expand Up @@ -143,6 +143,13 @@ Complete the `appsettings.json` file with the following content, replacing place
"Region": "garage",
"Secure": false
},
"Vault": {
"Enable": false,
"Address": "http://<vault-server-url>:8200",
"Token": "<vault-access-token>",
"Path": "electrostore",
"MountPoint": "secret"
},
"AllowedOrigins": [
"https://<your-frontend-domain1>",
"https://<your-frontend-domain2>"
Expand Down
11 changes: 9 additions & 2 deletions docs/01_installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Complete the `appsettings.json` file with the following content, replacing place
}
},
"ConnectionStrings": {
"DefaultConnection": "Server=mariadb;Port=3306;Database=electrostore;Uid=electrostore;Pwd=password;"
"DefaultConnection": "Server=mariadb;Port=3306;Database=electrostore;Uid=electrostore;Pwd=electrostore;"
},
"MQTT": {
"Username": "electrostore",
Expand All @@ -94,7 +94,7 @@ Complete the `appsettings.json` file with the following content, replacing place
"Port": 587,
"Username": "<your-email (optional)>",
"Password": "<your-email-password (optional)>",
"From": "<your-email (optional)>",
"From": "<your-email (optional)>"
},
"Jwt": {
"Key": "<your-random-key>",
Expand Down Expand Up @@ -127,6 +127,13 @@ Complete the `appsettings.json` file with the following content, replacing place
"Region": "garage",
"Secure": false
},
"Vault": {
"Enable": false,
"Address": "http://<vault-server-url>:8200",
"Token": "<vault-access-token>",
"Path": "electrostore",
"MountPoint": "secret"
},
"AllowedOrigins": [
"https://<your-frontend-domain1>",
"https://<your-frontend-domain2>"
Expand Down
8 changes: 7 additions & 1 deletion docs/generator/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,15 @@ <h3 style="margin: 1.5rem 0 1rem 0; color: #4caf50;">HashiCorp Vault Configurati

<div class="form-group">
<label for="vaultPath">Secrets Path</label>
<input type="text" id="vaultPath" name="vaultPath" placeholder="secret/electrostore" value="secret/electrostore">
<input type="text" id="vaultPath" name="vaultPath" placeholder="electrostore" value="secret/electrostore">
<small class="help-text">Path in Vault where secrets are stored</small>
</div>

<div class="form-group">
<label for="vaultMountPoint">Mount Point</label>
<input type="text" id="vaultMountPoint" name="vaultMountPoint" placeholder="secret" value="secret">
<small class="help-text">Mount point of the KV secrets engine</small>
</div>
</div>
</section>

Expand Down
3 changes: 2 additions & 1 deletion docs/generator/js/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ function collectConfig(formData) {
config.vault = {
addr: formData.get('vaultAddr') || 'http://vault:8200',
token: formData.get('vaultToken'),
path: formData.get('vaultPath') || 'secret/electrostore'
path: formData.get('vaultPath') || 'electrostore',
mountPoint: formData.get('vaultMountPoint') || 'secret'
};
}

Expand Down
46 changes: 22 additions & 24 deletions docs/generator/js/generators.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ networks:
}

if (!config.enableS3 || config.useMQTT || config.useMariaDB ||
(config.enableS3 && config.useS3) || (config.useVault && config.vault.integrated)) {
(config.enableS3 && config.useS3)) {
compose += `
volumes:`;
}
Expand All @@ -266,7 +266,6 @@ volumes:`;
if (config.useMariaDB) compose += `\n mariadb-data:`;
if (config.useMQTT) compose += `\n mqtt-data:`;
if (config.enableS3 && config.useS3) compose += `\n garage-data:\n garage-meta:`;
if (config.useVault && config.vault.integrated) compose += `\n vault-data:`;

return compose;
}
Expand Down Expand Up @@ -385,6 +384,20 @@ function generateAppsettings(config) {
});
}

if (config.useVault) {
settings.Vault = {
"Enable": true,
"Addr": config.vault.addr,
"Token": config.vault.token,
"Path": config.vault.path,
"MountPoint": config.vault.mountPoint
};
} else {
settings.Vault = {
"Enable": false
};
}

settings.FrontendUrl = config.frontUrl;
settings.AllowedOrigins = config.allowedOrigins;

Expand Down Expand Up @@ -444,15 +457,6 @@ function generateEnvFile(config) {
env += `S3_REGION=${config.s3.region}\n\n`;
}

if (config.useVault) {
env += `# HashiCorp Vault\n`;
env += `VAULT_TOKEN=${config.vault.token}\n`;
if (config.vault.integrated) {
env += `VAULT_VERSION=1.18\n`;
}
env += `\n`;
}

return env;
}

Expand All @@ -471,37 +475,31 @@ echo ""

`;

if (config.useVault && config.vault.integrated) {
if (config.useVault) {
script += `# Vault Configuration
echo "Configuring HashiCorp Vault..."

echo "Starting Vault..."
docker compose up -d vault

echo "Waiting for Vault to start (5 seconds)..."
sleep 5

echo "Enabling KV v2 engine..."
docker exec electrostore-vault vault secrets enable -version=2 -path=secret kv || echo "KV engine already enabled"
docker exec vault vault secrets enable -version=2 -path=${config.vault.mountPoint} kv || echo "KV engine already enabled"

echo "Storing secrets in Vault..."
`;

script += `docker exec electrostore-vault vault kv put ${config.vault.path} mariadb_password="
script += `docker exec vault vault kv put ${config.vault.mountPoint}/${config.vault.path} mariadb_password="
${config.useMariaDB ? config.mariadb.password : config.mariadbExternal.password}"
`;

script += `docker exec electrostore-vault vault kv patch ${config.vault.path} mqtt_password="
script += `docker exec vault vault kv patch ${config.vault.mountPoint}/${config.vault.path} mqtt_password="
${config.useMQTT ? config.mqtt.password : config.mqttExternal.password}
`;

if (config.enableSMTP && config.smtp) {
script += `docker exec electrostore-vault vault kv patch ${config.vault.path} smtp_password="
script += `docker exec vault vault kv patch ${config.vault.mountPoint}/${config.vault.path} smtp_password="
${config.smtp.password}"
`;
}

script += `docker exec electrostore-vault vault kv patch ${config.vault.path} jwt_key="
script += `docker exec vault vault kv patch ${config.vault.mountPoint}/${config.vault.path} jwt_key="
${config.jwt.key}"

echo "Vault configuration completed"
Expand Down Expand Up @@ -544,7 +542,7 @@ docker exec electrostore-garage /garage bucket allow --read --write ${config.s3.
if (config.useVault) {
script += `
echo "Storing S3 keys in Vault..."
docker exec electrostore-vault vault kv patch ${config.vault.path} s3_access_key="\$GARAGE_ACCESS_KEY" s3_secret_key="\$GARAGE_SECRET_KEY"
docker exec vault vault kv patch ${config.vault.path} s3_access_key="\$GARAGE_ACCESS_KEY" s3_secret_key="\$GARAGE_SECRET_KEY"
`;
}

Expand Down
72 changes: 72 additions & 0 deletions electrostoreAPI/Extensions/VaultConfigurationExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using VaultSharp;
using VaultSharp.V1.AuthMethods.Token;


namespace electrostore.Extensions;

public static class VaultConfigurationExtensions
{
public static IConfigurationBuilder AddVaultConfiguration(this IConfigurationBuilder builder)
{
var tempConfig = builder.Build();
if (tempConfig.GetSection("Vault:Enable").Get<bool>())
{
var vaultAddr = tempConfig.GetSection("Vault:Addr").Value ?? throw new InvalidOperationException("Vault:Addr configuration is missing.");
var vaultToken = tempConfig.GetSection("Vault:Token").Value ?? throw new InvalidOperationException("Vault:Token configuration is missing.");
var vaultPath = tempConfig.GetSection("Vault:Path").Value ?? throw new InvalidOperationException("Vault:Path configuration is missing.");
var vaultMountPoint = tempConfig.GetSection("Vault:MountPoint").Value ?? throw new InvalidOperationException("Vault:MountPoint configuration is missing.");
var authMethod = new TokenAuthMethodInfo(vaultToken);
var vaultConfig = new VaultClientSettings(vaultAddr, authMethod);
var vaultClient = new VaultClient(vaultConfig);
// in all config sections, replace values with vault secrets if they are in the format {{vault:<key>}}
foreach (var section in tempConfig.GetChildren())
{
foreach (var child in section.GetChildren())
{
if (child.Value != null && child.Value.Contains("{{vault:") && child.Value.Contains("}}"))
{
Console.WriteLine($"Checking key: {child.Key} with value: {child.Value}");
// for all occurrences of {{vault:<key>}} in the value, replace with the corresponding vault secret
var newValue = child.Value;
int startIndex = 0;
while (true)
{
int vaultStart = newValue.IndexOf("{{vault:", startIndex);
if (vaultStart == -1) break;
int vaultEnd = newValue.IndexOf("}}", vaultStart);
if (vaultEnd == -1) break;
var vaultKey = newValue.Substring(vaultStart + 8, vaultEnd - vaultStart - 8);
var secretValue = GetVaultSecret(vaultClient, vaultPath, vaultMountPoint, vaultKey);
newValue = string.Concat(newValue.AsSpan(0, vaultStart), secretValue, newValue.AsSpan(vaultEnd + 2));
startIndex = vaultStart + secretValue.Length;
}
// update the configuration with the new value
builder.AddInMemoryCollection(new Dictionary<string, string?>
{
{ child.Path, newValue }
});
}
}
}
}
return builder;
}
private static string GetVaultSecret(VaultClient vaultClient, string path, string mountPoint, string key)
{
try
{
var secret = vaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(path: path, mountPoint: mountPoint).GetAwaiter().GetResult();
if (secret.Data.Data.TryGetValue(key, out object? value))
{
return value?.ToString() ?? string.Empty;
}
return string.Empty;
}
catch (Exception ex)
{
Console.WriteLine($"Error reading secret from Vault: {ex.Message}");
Console.WriteLine($"Make sure the KV v2 engine is enabled and the secret exists at path: '{path}'");
throw new InvalidOperationException($"Failed to retrieve secret from Vault at path '{path}' for key '{key}': {ex.Message}", ex);
}
}
}
61 changes: 34 additions & 27 deletions electrostoreAPI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
using Microsoft.IdentityModel.JsonWebTokens;
using Minio;

using VaultSharp;
using VaultSharp.V1.AuthMethods.Token;

using MQTTnet;
using electrostore.Dto;
using electrostore.Enums;
Expand Down Expand Up @@ -45,6 +48,7 @@
using electrostore.Services.ValidateStoreService;
using electrostore.Services.JwtService;
using electrostore.Middleware;
using electrostore.Extensions;

using Microsoft.OpenApi.Models;
using Microsoft.AspNetCore.HttpOverrides;
Expand Down Expand Up @@ -249,10 +253,18 @@ private static void AddAuthentication(WebApplicationBuilder builder, byte[] key)

private static void AddScopes(WebApplicationBuilder builder)
{
// Add services to the container.
if (builder.Configuration.GetSection("Vault:Enable").Get<bool>())
{
var authMethod = new TokenAuthMethodInfo(builder.Configuration.GetSection("Vault:Token").Value);
var vaultConfig = new VaultClientSettings(builder.Configuration.GetSection("Vault:Addr").Value, authMethod);
builder.Services.AddSingleton<IVaultClient>(new VaultClient(vaultConfig));
builder.Configuration.AddVaultConfiguration();
}
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseMySql(builder.Configuration.GetConnectionString("DefaultConnection"), new MySqlServerVersion(new Version(11, 4, 7))));

options.UseMySql(builder.Configuration.GetConnectionString("DefaultConnection"),
new MySqlServerVersion(new Version(11, 4, 7))
)
);
builder.Services.AddSingleton<IMqttClient>(sp =>
{
var factory = new MqttClientFactory();
Expand All @@ -266,8 +278,6 @@ private static void AddScopes(WebApplicationBuilder builder)
mqttClient.ConnectAsync(options);
return mqttClient;
});

// Only register MinIO client if S3 is enabled
if (builder.Configuration.GetSection("S3:Enable").Get<bool>())
{
builder.Services.AddSingleton<IMinioClient>(sp =>
Expand All @@ -283,7 +293,6 @@ private static void AddScopes(WebApplicationBuilder builder)
return minioClient;
});
}

builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<IBoxService, BoxService>();
builder.Services.AddScoped<IBoxTagService, BoxTagService>();
Expand Down Expand Up @@ -349,29 +358,27 @@ private static void CreateRequiredDirectories()

private static void InitializeDatabase(WebApplication app)
{
using (var serviceScope = app.Services.CreateScope())
using var serviceScope = app.Services.CreateScope();
var context = serviceScope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// check if the database is up to date with the migrations
var pendingMigrations = context.Database.GetPendingMigrations();
if (pendingMigrations.Any())
{
var context = serviceScope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// check if the database is up to date with the migrations
var pendingMigrations = context.Database.GetPendingMigrations();
if (pendingMigrations.Any())
{
// Appliquer les migrations si nécessaire
context.Database.Migrate();
}
// check if the database is empty
if (!context.Users.Any())
context.Database.Migrate();
}
// check if the database is empty
if (!context.Users.Any())
{
var userService = serviceScope.ServiceProvider.GetRequiredService<IUserService>();
userService.CreateFirstAdminUser(new CreateUserDto
{
var userService = serviceScope.ServiceProvider.GetRequiredService<IUserService>();
userService.CreateFirstAdminUser(new CreateUserDto
{
nom_user = "Admin",
prenom_user = "Admin",
email_user = "admin@localhost.local",
mdp_user = "Admin@1234",
role_user = UserRole.Admin
}).Wait();
}
nom_user = "Admin",
prenom_user = "Admin",
email_user = "admin@localhost.local",
mdp_user = "Admin@1234",
role_user = UserRole.Admin
}).Wait();

}
}
}
Loading
Loading