The Estimate is validated
Some checks failed
Dotnet build and test / build (pull_request) Failing after 7m28s
Some checks failed
Dotnet build and test / build (pull_request) Failing after 7m28s
This commit is contained in:
parent
848f8be42f
commit
2484876c5b
20 changed files with 5392 additions and 218 deletions
|
|
@ -22,7 +22,7 @@ public partial class AuthenticationSettings : ObservableObject
|
|||
|
||||
public const string DefaultClientId = "postit";
|
||||
|
||||
public static readonly string[] DefaultScopes = { "blogs" };
|
||||
public static readonly string[] DefaultScopes = { "blogs", "api" };
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Authority { get; set; }
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ namespace PostIt.ViewModels;
|
|||
|
||||
public partial class Settings : ViewModelBase
|
||||
{
|
||||
public string SettingsFileName {get; private set;} = "postit-settings.json";
|
||||
[JsonIgnore]
|
||||
public string? SettingsFileFullName { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AuthenticationSettings Authentication { get; set; } = new();
|
||||
|
|
@ -39,6 +40,60 @@ public partial class Settings : ViewModelBase
|
|||
[JsonIgnore]
|
||||
public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Pret.");
|
||||
|
||||
|
||||
public bool Loaded { get; private set; } = false;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// True when the in-memory state has drifted from the last
|
||||
/// <see cref="Load"/> or <see cref="Save"/> snapshot. The
|
||||
/// Settings page binds the Sauver button's <c>IsEnabled</c> to
|
||||
/// this flag, so it only enables when the user has actually
|
||||
/// touched something since the last load / save. Cleared by
|
||||
/// <see cref="Load"/> (and by <see cref="ApplyJson"/>), set by
|
||||
/// every successful setter on the four top-level mutable
|
||||
/// properties and on the sub-properties of
|
||||
/// <see cref="Authentication"/>.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
public partial bool IsDirty { get; private set; } = false;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Guards every mutation of the observable state. <c>[ObservableProperty]</c>
|
||||
/// generates setters that call <c>SetProperty(...)</c> which fires
|
||||
/// <c>PropertyChanged</c>. Avalonia bindings consume that event on
|
||||
/// the UI thread, and a stray background-thread update is exactly
|
||||
/// what crashed <c>DataValidationErrors.SetErrors</c> on
|
||||
/// <c>postit://callback</c> re-launches. The lock makes mutations
|
||||
/// atomic; <see cref="OnPropertyChanged(PropertyChangedEventArgs)"/>
|
||||
/// then marshals the notification onto the UI thread so bindings
|
||||
/// observe the change on the right thread.
|
||||
/// </summary>
|
||||
private readonly object _mutationGate = new();
|
||||
|
||||
/// <summary>
|
||||
/// Scopes the PostIt client always requires from the OIDC provider,
|
||||
/// regardless of what the user has in their settings file.
|
||||
///
|
||||
/// <para>PostIt calls into the Blog API (and any other Yavsc API
|
||||
/// gated by an <c>[Authorize("…Scope")]</c> policy) and is silent
|
||||
/// about the contract: a missing scope here surfaces as a 401
|
||||
/// on the very first API call after login, with no obvious link
|
||||
/// to the settings. The "feature" scopes the user must opt into
|
||||
/// (e.g. <c>blogs</c>) are still their choice — we only force the
|
||||
/// structural ones that OIDC itself needs.</para>
|
||||
/// </summary>
|
||||
private static readonly string[] BuiltInScopes = new[]
|
||||
{
|
||||
"openid", // OIDC: required for the id_token
|
||||
"profile", // OIDC: standard profile claims
|
||||
"offline_access", // OIDC: required to receive a refresh_token
|
||||
"blogs",
|
||||
"api"
|
||||
};
|
||||
private readonly string DEFAULT_SETTINGS_FILENAME = "postit-settings.json";
|
||||
|
||||
public void SetActionStatus(string message, StatusSeverity severity = StatusSeverity.Info)
|
||||
{
|
||||
ActionStatus = severity switch
|
||||
|
|
@ -86,35 +141,6 @@ public partial class Settings : ViewModelBase
|
|||
MarkDirty();
|
||||
}
|
||||
|
||||
public bool Loaded { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// True when the in-memory state has drifted from the last
|
||||
/// <see cref="Load"/> or <see cref="Save"/> snapshot. The
|
||||
/// Settings page binds the Sauver button's <c>IsEnabled</c> to
|
||||
/// this flag, so it only enables when the user has actually
|
||||
/// touched something since the last load / save. Cleared by
|
||||
/// <see cref="Load"/> (and by <see cref="ApplyJson"/>), set by
|
||||
/// every successful setter on the four top-level mutable
|
||||
/// properties and on the sub-properties of
|
||||
/// <see cref="Authentication"/>.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
public partial bool IsDirty { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Guards every mutation of the observable state. <c>[ObservableProperty]</c>
|
||||
/// generates setters that call <c>SetProperty(...)</c> which fires
|
||||
/// <c>PropertyChanged</c>. Avalonia bindings consume that event on
|
||||
/// the UI thread, and a stray background-thread update is exactly
|
||||
/// what crashed <c>DataValidationErrors.SetErrors</c> on
|
||||
/// <c>postit://callback</c> re-launches. The lock makes mutations
|
||||
/// atomic; <see cref="OnPropertyChanged(PropertyChangedEventArgs)"/>
|
||||
/// then marshals the notification onto the UI thread so bindings
|
||||
/// observe the change on the right thread.
|
||||
/// </summary>
|
||||
private readonly object _mutationGate = new();
|
||||
|
||||
/// <summary>
|
||||
/// Build OidcClient options configured for Authorization Code + PKCE
|
||||
/// (no client secret). The browser implementation should be supplied
|
||||
|
|
@ -178,27 +204,6 @@ public partial class Settings : ViewModelBase
|
|||
Authentication.RefreshScopeListText();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scopes the PostIt client always requires from the OIDC provider,
|
||||
/// regardless of what the user has in their settings file.
|
||||
///
|
||||
/// <para>PostIt calls into the Blog API (and any other Yavsc API
|
||||
/// gated by an <c>[Authorize("…Scope")]</c> policy) and is silent
|
||||
/// about the contract: a missing scope here surfaces as a 401
|
||||
/// on the very first API call after login, with no obvious link
|
||||
/// to the settings. The "feature" scopes the user must opt into
|
||||
/// (e.g. <c>blogs</c>) are still their choice — we only force the
|
||||
/// structural ones that OIDC itself needs.</para>
|
||||
/// </summary>
|
||||
private static readonly string[] BuiltInScopes = new[]
|
||||
{
|
||||
"openid", // OIDC: required for the id_token
|
||||
"profile", // OIDC: standard profile claims
|
||||
"offline_access", // OIDC: required to receive a refresh_token
|
||||
"blogs",
|
||||
"api"
|
||||
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Merge user-configured scopes with the built-in ones. User scopes
|
||||
|
|
@ -255,7 +260,14 @@ public partial class Settings : ViewModelBase
|
|||
&& !string.IsNullOrWhiteSpace(envJson))
|
||||
{
|
||||
Console.WriteLine("🔎 Loading settings from POSTIT_SETTINGS_JSON environment variable.");
|
||||
ApplyJson(envJson, "POSTIT_SETTINGS_JSON");
|
||||
FileInfo configByEnvFileInfo = new FileInfo(envJson);
|
||||
if (!configByEnvFileInfo.Exists)
|
||||
{
|
||||
throw new Exception($"🩎 Settings file not found at {configByEnvFileInfo.FullName}");
|
||||
}
|
||||
string json = File.ReadAllText(configByEnvFileInfo.FullName);
|
||||
ApplyJson(json, "POSTIT_SETTINGS_JSON");
|
||||
SettingsFileFullName = configByEnvFileInfo.FullName;
|
||||
Loaded = true;
|
||||
return;
|
||||
}
|
||||
|
|
@ -264,9 +276,22 @@ public partial class Settings : ViewModelBase
|
|||
"PostIt"
|
||||
);
|
||||
|
||||
string configPath = Path.Combine(configDir, SettingsFileName);
|
||||
if (SettingsFileFullName is not null)
|
||||
{
|
||||
// Already set by a previous Load() or by the environment
|
||||
// variable path above. Use it as-is.
|
||||
}
|
||||
else if (Environment.GetEnvironmentVariable("POSTIT_SETTINGS_JSON") is string envPath
|
||||
&& !string.IsNullOrWhiteSpace(envPath))
|
||||
{
|
||||
SettingsFileFullName = envPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
SettingsFileFullName = Path.Combine(configDir, "postit-settings.json");
|
||||
}
|
||||
|
||||
FileInfo configFileInfo = new FileInfo(configPath);
|
||||
FileInfo configFileInfo = new FileInfo(SettingsFileFullName);
|
||||
|
||||
if (!configFileInfo.Exists)
|
||||
{
|
||||
|
|
@ -295,6 +320,7 @@ public partial class Settings : ViewModelBase
|
|||
using var reader = new StreamReader(stream);
|
||||
var json = reader.ReadToEnd();
|
||||
ApplyJson(json, $"user file {configFileInfo.FullName}");
|
||||
SettingsFileFullName = configFileInfo.FullName;
|
||||
Loaded = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -458,11 +484,17 @@ public partial class Settings : ViewModelBase
|
|||
{
|
||||
SetActionStatus("Enregistrement des parametres...", StatusSeverity.Info);
|
||||
|
||||
var configDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"PostIt");
|
||||
Directory.CreateDirectory(configDir);
|
||||
var configPath = Path.Combine(configDir, SettingsFileName);
|
||||
if (SettingsFileFullName is null)
|
||||
{
|
||||
var configDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"PostIt");
|
||||
Directory.CreateDirectory(configDir);
|
||||
SettingsFileFullName = Path.Combine(configDir, DEFAULT_SETTINGS_FILENAME);
|
||||
}
|
||||
|
||||
var configPath = SettingsFileFullName!;
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(configPath)!);
|
||||
|
||||
lock (_mutationGate)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"Authentication": {
|
||||
"ClientId": "postit",
|
||||
"Authority": "https://yavsc.pschneider.fr"
|
||||
},
|
||||
"RedirectUri": "postit://callback",
|
||||
"DarkMode": false,
|
||||
"ApiUrl": "https://api.pschneider.fr/api/v1/",
|
||||
"Scopes": [
|
||||
"openid",
|
||||
"profile",
|
||||
"offline_access",
|
||||
"blogs"
|
||||
]
|
||||
}
|
||||
17
src/PostIt/PostIt/postit-settings-sample.json
Normal file
17
src/PostIt/PostIt/postit-settings-sample.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"Authentication": {
|
||||
"ClientId": "postit",
|
||||
"Authority": "https://yavsc.pschneider.fr",
|
||||
"Scopes": [
|
||||
"openid",
|
||||
"profile",
|
||||
"offline_access",
|
||||
"blogs",
|
||||
"api"
|
||||
],
|
||||
"RedirectUri": "postit://callback"
|
||||
},
|
||||
"DarkMode": false,
|
||||
"ApiUrl": "https://api.pschneider.fr/api/v1/"
|
||||
|
||||
}
|
||||
|
|
@ -1,15 +1,21 @@
|
|||
{
|
||||
"Authentication": {
|
||||
"ClientId": "postit",
|
||||
"Authority": "https://yavsc.pschneider.fr/"
|
||||
},
|
||||
"RedirectUri": "postit://callback",
|
||||
"DarkMode": true,
|
||||
"ApiUrl": "https://api.pschneider.fr/api/v1/",
|
||||
"Authentication": {
|
||||
"Authority": "https://localhost:5001",
|
||||
"ClientId": "postit",
|
||||
"Scopes": [
|
||||
"openid",
|
||||
"profile",
|
||||
"offline_access",
|
||||
"blogs"
|
||||
]
|
||||
}
|
||||
"blogs",
|
||||
"api"
|
||||
],
|
||||
"RedirectUri": "postit://callback"
|
||||
},
|
||||
"DarkMode": true,
|
||||
"BlogsApiUrl": "https://localhost:5003/api/v1/",
|
||||
"ApiUrl": "https://localhost:5005/api/v1/",
|
||||
"SearchText": "",
|
||||
"ProviderOngoingRequestsSortOption": "",
|
||||
"Loaded": true,
|
||||
"IsDirty": true,
|
||||
"CanNavigateNext": false,
|
||||
"CanNavigatePrevious": true,
|
||||
"SaveCommand": {}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue