Migrated the store secret calls from Tauri JS to the runtime API

This commit is contained in:
Thorsten Sommer 2024-08-29 08:56:06 +02:00
parent 97854e7eaa
commit a224d2ab10
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
8 changed files with 83 additions and 41 deletions

View File

@ -1,13 +1,14 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Host = AIStudio.Provider.SelfHosted.Host; using Host = AIStudio.Provider.SelfHosted.Host;
using RustService = AIStudio.Tools.RustService; using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Settings; namespace AIStudio.Dialogs;
/// <summary> /// <summary>
/// The provider settings dialog. /// The provider settings dialog.
@ -83,9 +84,6 @@ public partial class ProviderDialog : ComponentBase
[Inject] [Inject]
private RustService RustService { get; init; } = null!; private RustService RustService { get; init; } = null!;
[Inject]
private Encryption Encryption { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new(); private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
/// <summary> /// <summary>
@ -104,8 +102,9 @@ public partial class ProviderDialog : ComponentBase
private MudForm form = null!; private MudForm form = null!;
private readonly List<Model> availableModels = new(); private readonly List<Model> availableModels = new();
private readonly Encryption encryption = Program.ENCRYPTION;
private Provider CreateProviderSettings() => new() private Settings.Provider CreateProviderSettings() => new()
{ {
Num = this.DataNum, Num = this.DataNum,
Id = this.DataId, Id = this.DataId,
@ -154,7 +153,7 @@ public partial class ProviderDialog : ComponentBase
var requestedSecret = await this.RustService.GetAPIKey(provider); var requestedSecret = await this.RustService.GetAPIKey(provider);
if(requestedSecret.Success) if(requestedSecret.Success)
{ {
this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.Encryption); this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption);
// Now, we try to load the list of available models: // Now, we try to load the list of available models:
await this.ReloadModels(); await this.ReloadModels();
@ -200,7 +199,7 @@ public partial class ProviderDialog : ComponentBase
var provider = addedProviderSettings.CreateProvider(this.Logger, this.RustService); var provider = addedProviderSettings.CreateProvider(this.Logger, this.RustService);
// Store the API key in the OS secure storage: // Store the API key in the OS secure storage:
var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey); var storeResponse = await this.RustService.SetAPIKey(provider, this.dataAPIKey);
if (!storeResponse.Success) if (!storeResponse.Success)
{ {
this.dataAPIKeyStorageIssue = $"Failed to store the API key in the operating system. The message was: {storeResponse.Issue}. Please try again."; this.dataAPIKeyStorageIssue = $"Failed to store the API key in the operating system. The message was: {storeResponse.Issue}. Please try again.";

View File

@ -42,24 +42,6 @@ public sealed class SettingsManager(ILogger<SettingsManager> logger)
#region API Key Handling #region API Key Handling
private readonly record struct StoreSecretRequest(string Destination, string UserName, string Secret);
/// <summary>
/// Data structure for storing a secret response.
/// </summary>
/// <param name="Success">True, when the secret was successfully stored.</param>
/// <param name="Issue">The issue, when the secret could not be stored.</param>
public readonly record struct StoreSecretResponse(bool Success, string Issue);
/// <summary>
/// Try to store the API key for the given provider.
/// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="provider">The provider to store the API key for.</param>
/// <param name="key">The API key to store.</param>
/// <returns>The store secret response.</returns>
public async Task<StoreSecretResponse> SetAPIKey(IJSRuntime jsRuntime, IProvider provider, string key) => await jsRuntime.InvokeAsync<StoreSecretResponse>("window.__TAURI__.invoke", "store_secret", new StoreSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName, key));
private readonly record struct DeleteSecretRequest(string Destination, string UserName); private readonly record struct DeleteSecretRequest(string Destination, string UserName);
/// <summary> /// <summary>

View File

@ -1,7 +1,6 @@
using System.Text.Json.Serialization;
namespace AIStudio.Tools.Rust; namespace AIStudio.Tools.Rust;
public readonly record struct GetSecretRequest( public readonly record struct GetSecretRequest(
string Destination, string Destination,
[property:JsonPropertyName("user_name")] string UserName); string UserName
);

View File

@ -0,0 +1,3 @@
namespace AIStudio.Tools.Rust;
public readonly record struct StoreSecretRequest(string Destination, string UserName, EncryptedText Secret);

View File

@ -0,0 +1,8 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// Data structure for storing a secret response.
/// </summary>
/// <param name="Success">True, when the secret was successfully stored.</param>
/// <param name="Issue">The issue, when the secret could not be stored.</param>
public readonly record struct StoreSecretResponse(bool Success, string Issue);

View File

@ -1,3 +1,5 @@
using System.Text.Json;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Tools.Rust; using AIStudio.Tools.Rust;
@ -15,6 +17,11 @@ public sealed class RustService(string apiPort) : IDisposable
BaseAddress = new Uri($"http://127.0.0.1:{apiPort}"), BaseAddress = new Uri($"http://127.0.0.1:{apiPort}"),
}; };
private readonly JsonSerializerOptions jsonRustSerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
};
private ILogger<RustService>? logger; private ILogger<RustService>? logger;
private Encryption? encryptor; private Encryption? encryptor;
@ -164,7 +171,7 @@ public sealed class RustService(string apiPort) : IDisposable
public async Task<RequestedSecret> GetAPIKey(IProvider provider) public async Task<RequestedSecret> GetAPIKey(IProvider provider)
{ {
var secretRequest = new GetSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName); var secretRequest = new GetSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName);
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest); var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode) if (!result.IsSuccessStatusCode)
{ {
this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}' due to an API issue: '{result.StatusCode}'"); this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}' due to an API issue: '{result.StatusCode}'");
@ -178,6 +185,31 @@ public sealed class RustService(string apiPort) : IDisposable
return secret; return secret;
} }
/// <summary>
/// Try to store the API key for the given provider.
/// </summary>
/// <param name="provider">The provider to store the API key for.</param>
/// <param name="key">The API key to store.</param>
/// <returns>The store secret response.</returns>
public async Task<StoreSecretResponse> SetAPIKey(IProvider provider, string key)
{
var encryptedKey = await this.encryptor!.Encrypt(key);
var request = new StoreSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName, encryptedKey);
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to store the API key for provider '{provider.Id}' due to an API issue: '{result.StatusCode}'");
return new StoreSecretResponse(false, "Failed to get the API key due to an API issue.");
}
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>();
if (!state.Success)
this.logger!.LogError($"Failed to store the API key for provider '{provider.Id}': '{state.Issue}'");
return state;
}
#region IDisposable #region IDisposable
public void Dispose() public void Dispose()

View File

@ -194,7 +194,7 @@ async fn main() {
// //
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
_ = rocket::custom(figment) _ = rocket::custom(figment)
.mount("/", routes![dotnet_port, dotnet_ready, set_clipboard, check_for_update, install_update, get_secret]) .mount("/", routes![dotnet_port, dotnet_ready, set_clipboard, check_for_update, install_update, get_secret, store_secret])
.ignite().await.unwrap() .ignite().await.unwrap()
.launch().await.unwrap(); .launch().await.unwrap();
}); });
@ -319,7 +319,7 @@ async fn main() {
}) })
.plugin(tauri_plugin_window_state::Builder::default().build()) .plugin(tauri_plugin_window_state::Builder::default().build())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
store_secret, delete_secret delete_secret
]) ])
.build(tauri::generate_context!()) .build(tauri::generate_context!())
.expect("Error while running Tauri application"); .expect("Error while running Tauri application");
@ -746,30 +746,49 @@ async fn install_update() {
} }
} }
#[tauri::command] #[post("/secrets/store", data = "<request>")]
fn store_secret(destination: String, user_name: String, secret: String) -> StoreSecretResponse { fn store_secret(request: Json<StoreSecret>) -> Json<StoreSecretResponse> {
let service = format!("mindwork-ai-studio::{}", destination); let user_name = request.user_name.as_str();
let entry = Entry::new(service.as_str(), user_name.as_str()).unwrap(); let decrypted_text = match ENCRYPTION.decrypt(&request.secret) {
let result = entry.set_password(secret.as_str()); Ok(text) => text,
Err(e) => {
error!(Source = "Secret Store"; "Failed to decrypt the text: {e}.");
return Json(StoreSecretResponse {
success: false,
issue: format!("Failed to decrypt the text: {e}"),
})
},
};
let service = format!("mindwork-ai-studio::{}", request.destination);
let entry = Entry::new(service.as_str(), user_name).unwrap();
let result = entry.set_password(decrypted_text.as_str());
match result { match result {
Ok(_) => { Ok(_) => {
info!(Source = "Secret Store"; "Secret for {service} and user {user_name} was stored successfully."); info!(Source = "Secret Store"; "Secret for {service} and user {user_name} was stored successfully.");
StoreSecretResponse { Json(StoreSecretResponse {
success: true, success: true,
issue: String::from(""), issue: String::from(""),
} })
}, },
Err(e) => { Err(e) => {
error!(Source = "Secret Store"; "Failed to store secret for {service} and user {user_name}: {e}."); error!(Source = "Secret Store"; "Failed to store secret for {service} and user {user_name}: {e}.");
StoreSecretResponse { Json(StoreSecretResponse {
success: false, success: false,
issue: e.to_string(), issue: e.to_string(),
} })
}, },
} }
} }
#[derive(Deserialize)]
struct StoreSecret {
destination: String,
user_name: String,
secret: EncryptedText,
}
#[derive(Serialize)] #[derive(Serialize)]
struct StoreSecretResponse { struct StoreSecretResponse {
success: bool, success: bool,