2025-02-09 12:36:37 +01:00
using AIStudio.Assistants.ERI ;
2025-05-18 21:31:15 +02:00
using AIStudio.Components ;
2025-01-13 19:51:26 +01:00
using AIStudio.Settings.DataModel ;
2025-02-09 12:36:37 +01:00
using AIStudio.Tools.ERIClient ;
using AIStudio.Tools.ERIClient.DataModel ;
2025-02-15 15:41:12 +01:00
using AIStudio.Tools.Services ;
2025-01-13 19:51:26 +01:00
using AIStudio.Tools.Validation ;
using Microsoft.AspNetCore.Components ;
2025-03-08 21:04:17 +01:00
using RetrievalInfo = AIStudio . Tools . ERIClient . DataModel . RetrievalInfo ;
2025-01-13 19:51:26 +01:00
// ReSharper disable InconsistentNaming
namespace AIStudio.Dialogs ;
2025-05-18 21:31:15 +02:00
public partial class DataSourceERI_V1Dialog : MSGComponentBase , ISecretId
2025-01-13 19:51:26 +01:00
{
[CascadingParameter]
2025-03-12 19:12:56 +01:00
private IMudDialogInstance MudDialog { get ; set ; } = null !;
2025-01-13 19:51:26 +01:00
[Parameter]
public bool IsEditing { get ; set ; }
[Parameter]
public DataSourceERI_V1 DataSource { get ; set ; }
[Inject]
private ILogger < ProviderDialog > Logger { get ; init ; } = null !;
[Inject]
private RustService RustService { get ; init ; } = null !;
private static readonly Dictionary < string , object? > SPELLCHECK_ATTRIBUTES = new ();
private readonly DataSourceValidation dataSourceValidation ;
private readonly Encryption encryption = Program . ENCRYPTION ;
/// <summary>
/// The list of used data source names. We need this to check for uniqueness.
/// </summary>
private List < string > UsedDataSourcesNames { get ; set ; } = [];
private bool dataIsValid ;
private string [] dataIssues = [];
private string dataSecretStorageIssue = string . Empty ;
private string dataEditingPreviousInstanceName = string . Empty ;
private List < AuthMethod > availableAuthMethods = [];
2025-02-15 15:41:12 +01:00
private DataSourceSecurity dataSecurityPolicy ;
private SecurityRequirements dataSourceSecurityRequirements ;
2025-05-26 19:53:31 +02:00
private ushort dataMaxMatches = 10 ;
2025-01-13 19:51:26 +01:00
private bool connectionTested ;
private bool connectionSuccessfulTested ;
private uint dataNum ;
private string dataSecret = string . Empty ;
private string dataId = Guid . NewGuid (). ToString ();
private string dataName = string . Empty ;
private string dataHostname = string . Empty ;
private int dataPort ;
private AuthMethod dataAuthMethod ;
private string dataUsername = string . Empty ;
2025-03-08 21:04:17 +01:00
private List < RetrievalInfo > availableRetrievalProcesses = [];
private RetrievalInfo dataSelectedRetrievalProcess ;
2025-01-13 19:51:26 +01:00
// We get the form reference from Blazor code to validate it manually:
private MudForm form = null !;
public DataSourceERI_V1Dialog ()
{
this . dataSourceValidation = new ()
{
GetAuthMethod = () => this . dataAuthMethod ,
GetPreviousDataSourceName = () => this . dataEditingPreviousInstanceName ,
GetUsedDataSourceNames = () => this . UsedDataSourcesNames ,
GetSecretStorageIssue = () => this . dataSecretStorageIssue ,
GetTestedConnection = () => this . connectionTested ,
GetTestedConnectionResult = () => this . connectionSuccessfulTested ,
GetAvailableAuthMethods = () => this . availableAuthMethods ,
2025-02-15 15:41:12 +01:00
GetSecurityRequirements = () => this . dataSourceSecurityRequirements ,
2025-01-13 19:51:26 +01:00
};
}
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync ()
{
// Configure the spellchecking for the instance name input:
this . SettingsManager . InjectSpellchecking ( SPELLCHECK_ATTRIBUTES );
// Load the used instance names:
this . UsedDataSourcesNames = this . SettingsManager . ConfigurationData . DataSources . Select ( x => x . Name . ToLowerInvariant ()). ToList ();
// When editing, we need to load the data:
if ( this . IsEditing )
{
2025-03-08 21:04:17 +01:00
//
// Assign the data to the form fields:
//
2025-01-13 19:51:26 +01:00
this . dataEditingPreviousInstanceName = this . DataSource . Name . ToLowerInvariant ();
this . dataNum = this . DataSource . Num ;
this . dataId = this . DataSource . Id ;
this . dataName = this . DataSource . Name ;
this . dataHostname = this . DataSource . Hostname ;
this . dataPort = this . DataSource . Port ;
this . dataAuthMethod = this . DataSource . AuthMethod ;
this . dataUsername = this . DataSource . Username ;
2025-02-15 15:41:12 +01:00
this . dataSecurityPolicy = this . DataSource . SecurityPolicy ;
2025-05-26 19:53:31 +02:00
this . dataMaxMatches = this . DataSource . MaxMatches ;
2025-06-27 22:03:51 +02:00
// We cannot load the retrieval processes now, since we have
// to load the data first. But while doing so, we can need to
// restore the selected retrieval process id. That's why we
// assign the selected retrieval id to the default retrieval process:
this . dataSelectedRetrievalProcess = this . dataSelectedRetrievalProcess with { Id = this . DataSource . SelectedRetrievalId };
2025-01-13 19:51:26 +01:00
if ( this . dataAuthMethod is AuthMethod . TOKEN or AuthMethod . USERNAME_PASSWORD )
{
// Load the secret:
2026-05-18 16:26:51 +02:00
var requestedSecret = await this . RustService . GetSecret ( this , SecretStoreType . DATA_SOURCE );
2025-01-13 19:51:26 +01:00
if ( requestedSecret . Success )
this . dataSecret = await requestedSecret . Secret . Decrypt ( this . encryption );
else
{
this . dataSecret = string . Empty ;
2025-05-31 12:04:58 +02:00
this . dataSecretStorageIssue = string . Format ( T ( "Failed to load the auth. secret from the operating system. The message was: {0}. You might ignore this message and provide the secret again." ), requestedSecret . Issue );
2025-01-13 19:51:26 +01:00
await this . form . Validate ();
}
}
2025-03-08 21:04:17 +01:00
// Load the data:
await this . TestConnection ();
// Select the retrieval process:
this . dataSelectedRetrievalProcess = this . availableRetrievalProcesses . FirstOrDefault ( n => n . Id == this . DataSource . SelectedRetrievalId );
2025-01-13 19:51:26 +01:00
}
await base . OnInitializedAsync ();
}
protected override async Task OnAfterRenderAsync ( bool firstRender )
{
// Reset the validation when not editing and on the first render.
// We don't want to show validation errors when the user opens the dialog.
if (! this . IsEditing && firstRender )
this . form . ResetValidation ();
await base . OnAfterRenderAsync ( firstRender );
}
#endregion
#region Implementation of ISecretId
public string SecretId => this . dataId ;
public string SecretName => this . dataName ;
#endregion
private DataSourceERI_V1 CreateDataSource ()
{
var cleanedHostname = this . dataHostname . Trim ();
return new DataSourceERI_V1
{
Id = this . dataId ,
Num = this . dataNum ,
Port = this . dataPort ,
Name = this . dataName ,
Hostname = cleanedHostname . EndsWith ( '/' ) ? cleanedHostname [..^ 1 ] : cleanedHostname ,
AuthMethod = this . dataAuthMethod ,
Username = this . dataUsername ,
2026-05-18 16:26:51 +02:00
UsernamePasswordMode = DataSourceERIUsernamePasswordMode . USER_MANAGED ,
2025-01-13 19:51:26 +01:00
Type = DataSourceType . ERI_V1 ,
2025-02-15 15:41:12 +01:00
SecurityPolicy = this . dataSecurityPolicy ,
2025-03-08 21:04:17 +01:00
SelectedRetrievalId = this . dataSelectedRetrievalProcess . Id ,
2025-05-26 19:53:31 +02:00
MaxMatches = this . dataMaxMatches ,
2025-01-13 19:51:26 +01:00
};
}
private bool IsConnectionEncrypted () => this . dataHostname . StartsWith ( "https://" , StringComparison . InvariantCultureIgnoreCase );
private bool IsConnectionPossible ()
{
if ( this . dataSourceValidation . ValidatingHostname ( this . dataHostname ) is not null )
return false ;
if ( this . dataSourceValidation . ValidatePort ( this . dataPort ) is not null )
return false ;
return true ;
}
private async Task TestConnection ()
{
try
{
2025-02-09 12:36:37 +01:00
var cts = new CancellationTokenSource ( TimeSpan . FromSeconds ( 14 ));
2025-03-11 13:57:47 +01:00
this . DataSource = this . CreateDataSource ();
using var client = ERIClientFactory . Get ( ERIVersion . V1 , this . DataSource );
2025-02-09 12:36:37 +01:00
if ( client is null )
{
await this . form . Validate ();
Array . Resize ( ref this . dataIssues , this . dataIssues . Length + 1 );
2025-05-18 21:31:15 +02:00
this . dataIssues [^ 1 ] = T ( "Failed to connect to the ERI v1 server. The server is not supported." );
2025-02-09 12:36:37 +01:00
return ;
}
2025-01-13 19:51:26 +01:00
2025-02-09 12:36:37 +01:00
var authSchemes = await client . GetAuthMethodsAsync ( cts . Token );
if (! authSchemes . Successful )
2025-01-13 19:51:26 +01:00
{
2025-02-09 12:36:37 +01:00
await this . form . Validate ();
Array . Resize ( ref this . dataIssues , this . dataIssues . Length + 1 );
this . dataIssues [^ 1 ] = authSchemes . Message ;
return ;
}
2025-01-13 19:51:26 +01:00
2025-02-09 12:36:37 +01:00
this . availableAuthMethods = authSchemes . Data !. Select ( n => n . AuthMethod ). ToList ();
2025-01-13 19:51:26 +01:00
2025-03-11 13:57:47 +01:00
var loginResult = await client . AuthenticateAsync ( this . RustService , this . dataSecret , cts . Token );
2025-02-15 15:41:12 +01:00
if (! loginResult . Successful )
{
await this . form . Validate ();
Array . Resize ( ref this . dataIssues , this . dataIssues . Length + 1 );
this . dataIssues [^ 1 ] = loginResult . Message ;
return ;
}
var securityRequirementsRequest = await client . GetSecurityRequirementsAsync ( cts . Token );
if (! securityRequirementsRequest . Successful )
{
await this . form . Validate ();
Array . Resize ( ref this . dataIssues , this . dataIssues . Length + 1 );
this . dataIssues [^ 1 ] = securityRequirementsRequest . Message ;
return ;
}
this . dataSourceSecurityRequirements = securityRequirementsRequest . Data ;
2025-03-08 21:04:17 +01:00
var retrievalInfoRequest = await client . GetRetrievalInfoAsync ( cts . Token );
if (! retrievalInfoRequest . Successful )
{
await this . form . Validate ();
Array . Resize ( ref this . dataIssues , this . dataIssues . Length + 1 );
this . dataIssues [^ 1 ] = retrievalInfoRequest . Message ;
return ;
}
this . availableRetrievalProcesses = retrievalInfoRequest . Data ?? [];
2025-02-15 15:41:12 +01:00
2025-02-09 12:36:37 +01:00
this . connectionTested = true ;
this . connectionSuccessfulTested = true ;
this . Logger . LogInformation ( "Connection to the ERI v1 server was successful tested." );
2025-01-13 19:51:26 +01:00
}
catch ( Exception e )
{
await this . form . Validate ();
Array . Resize ( ref this . dataIssues , this . dataIssues . Length + 1 );
2025-05-18 21:31:15 +02:00
this . dataIssues [^ 1 ] = string . Format ( T ( "Failed to connect to the ERI v1 server. The message was: {0}" ), e . Message );
2025-01-13 19:51:26 +01:00
this . Logger . LogError ( $"Failed to connect to the ERI v1 server. Message: {e.Message}" );
this . connectionTested = true ;
this . connectionSuccessfulTested = false ;
}
}
private string GetTestResultText ()
{
if (! this . connectionTested )
2025-05-18 21:31:15 +02:00
return T ( "Not tested yet." );
2025-01-13 19:51:26 +01:00
2025-05-18 21:31:15 +02:00
return this . connectionSuccessfulTested ? T ( "Connection successful." ) : T ( "Connection failed." );
2025-01-13 19:51:26 +01:00
}
private Color GetTestResultColor ()
{
if (! this . connectionTested )
return Color . Default ;
return this . connectionSuccessfulTested ? Color . Success : Color . Error ;
}
private string GetTestResultIcon ()
{
if (! this . connectionTested )
return Icons . Material . Outlined . HourglassEmpty ;
return this . connectionSuccessfulTested ? Icons . Material . Outlined . CheckCircle : Icons . Material . Outlined . Error ;
}
private bool NeedsSecret () => this . dataAuthMethod is AuthMethod . TOKEN or AuthMethod . USERNAME_PASSWORD ;
private string GetSecretLabel () => this . dataAuthMethod switch
{
2025-05-18 21:31:15 +02:00
AuthMethod . TOKEN => T ( "Access Token" ),
AuthMethod . USERNAME_PASSWORD => T ( "Password" ),
_ => T ( "Secret" ),
2025-01-13 19:51:26 +01:00
};
private async Task Store ()
{
await this . form . Validate ();
var testConnectionValidation = this . dataSourceValidation . ValidateTestedConnection ();
if ( testConnectionValidation is not null )
{
Array . Resize ( ref this . dataIssues , this . dataIssues . Length + 1 );
this . dataIssues [^ 1 ] = testConnectionValidation ;
this . dataIsValid = false ;
}
this . dataSecretStorageIssue = string . Empty ;
// When the data is not valid, we don't store it:
if (! this . dataIsValid )
return ;
var addedDataSource = this . CreateDataSource ();
if (! string . IsNullOrWhiteSpace ( this . dataSecret ))
{
// Store the secret in the OS secure storage:
2026-05-18 16:26:51 +02:00
var storeResponse = await this . RustService . SetSecret ( this , this . dataSecret , SecretStoreType . DATA_SOURCE );
2025-01-13 19:51:26 +01:00
if (! storeResponse . Success )
{
2025-05-18 21:31:15 +02:00
this . dataSecretStorageIssue = string . Format ( T ( "Failed to store the auth. secret in the operating system. The message was: {0}. Please try again." ), storeResponse . Issue );
2025-01-13 19:51:26 +01:00
await this . form . Validate ();
return ;
}
}
this . MudDialog . Close ( DialogResult . Ok ( addedDataSource ));
}
private void Cancel () => this . MudDialog . Cancel ();
}