Identity reloaded

This commit is contained in:
Paul Schneider 2026-07-05 23:56:10 +01:00
commit 333b066e66
33 changed files with 195 additions and 180 deletions

View file

@ -13,9 +13,9 @@
--> -->
<ItemGroup> <ItemGroup>
<PackageVersion Include="coverlet.collector" Version="10.0.1" /> <PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="GitVersion.MsBuild" Version="6.7.0" /> <PackageVersion Include="GitVersion.MsBuild" Version="6.8.1" />
<PackageVersion Include="HigginsSoft.IdentityServer8" Version="8.0.5-preview-net9" /> <PackageVersion Include="HigginsSoft.IdentityServer8" Version="8.1.0-alpha.171" />
<PackageVersion Include="HigginsSoft.IdentityServer8.EntityFramework" Version="8.0.5-preview-net9" /> <PackageVersion Include="HigginsSoft.IdentityServer8.EntityFramework" Version="8.1.0-alpha.171" />
<PackageVersion Include="IdentityModel.OidcClient" Version="6.0.0" /> <PackageVersion Include="IdentityModel.OidcClient" Version="6.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" /> <PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.9" /> <PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.9" />
@ -24,10 +24,10 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.9" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="10.0.2" /> <PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="10.0.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" /> <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" /> <PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="YamlDotNet" Version="18.1.0" /> <PackageVersion Include="YamlDotNet" Version="18.1.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -26,7 +26,7 @@ COPY src/PostIt/PostIt.Desktop/*.csproj ./src/PostIt/PostIt.Desktop/
COPY . . COPY . .
# 3. Restauration des dépendances avec vos workloads actifs # 3. Restauration des dépendances avec vos workloads actifs
RUN dotnet nuget add source https://isn.pschneider.fr/v3/index.json --allow-insecure-connections RUN dotnet nuget add source https://isn.pschneider.fr/api/v3/index.json
# 4. Restauration des dépendances pour tous les projets # 4. Restauration des dépendances pour tous les projets
RUN dotnet restore RUN dotnet restore

View file

@ -10,8 +10,8 @@ include .env
all: all:
dotnet build --nologo dotnet build --nologo
clean: clean:
dotnet clean dotnet clean -c $(CONFIG)
src/Yavsc/bin/output/wwwroot: src/Yavsc/bin/output/wwwroot:
dotnet --project src/Yavsc.Org/Yavsc.Org.csproj publish dotnet --project src/Yavsc.Org/Yavsc.Org.csproj publish
@ -31,7 +31,7 @@ src/Yavsc.Server/bin/$(CONFIG)/$(FRAMEWORK)/Yavsc.Server.dll:
src/Yavsc/bin/$(CONFIG)/$(FRAMEWORK)/Yavsc.dll: src/Yavsc/bin/$(CONFIG)/$(FRAMEWORK)/Yavsc.dll:
dotnet build -p:Configuration=$(CONFIG) --project src/Yavsc.Org/Yavsc.Org.csproj dotnet build -p:Configuration=$(CONFIG) --project src/Yavsc.Org/Yavsc.Org.csproj
$(DESTDIR): $(DESTDIR):
mkdir $(DESTDIR) mkdir $(DESTDIR)
install: $(DESTDIR) install: $(DESTDIR)

View file

@ -10,7 +10,7 @@ namespace PostIt.Tests;
/// URL emitted by OidcClient, extracts its <c>state</c>, and returns a /// URL emitted by OidcClient, extracts its <c>state</c>, and returns a
/// BrowserResult that mimics the OIDC redirect-with-code callback. /// BrowserResult that mimics the OIDC redirect-with-code callback.
/// ///
/// The paired <see cref="OidcStubAuthority"/>'s token endpoint accepts /// The paired <see cref="OIDCStubAuthority"/>'s token endpoint accepts
/// any authorization code, so we don't need to mint a real one here. /// any authorization code, so we don't need to mint a real one here.
/// </summary> /// </summary>
public sealed class FakeAuthorizingBrowser public sealed class FakeAuthorizingBrowser

View file

@ -14,7 +14,7 @@ public class LoginPageViewModelTests
// short-circuits the system browser. The authority signs its // short-circuits the system browser. The authority signs its
// access_token with RS256; the fake browser captures the redirect // access_token with RS256; the fake browser captures the redirect
// URI so the authority can complete the token exchange. // URI so the authority can complete the token exchange.
using var authority = await OidcStubAuthority.StartAsync(); using var authority = await OIDCStubAuthority.StartAsync();
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri); var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
var settings = new PostIt.Settings var settings = new PostIt.Settings
@ -96,7 +96,7 @@ public class LoginPageViewModelTests
// double slash before /.well-known/openid-configuration. The // double slash before /.well-known/openid-configuration. The
// stub advertises itself without the trailing slash; OidcClient // stub advertises itself without the trailing slash; OidcClient
// must bridge. // must bridge.
using var authority = await OidcStubAuthority.StartAsync(); using var authority = await OIDCStubAuthority.StartAsync();
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri); var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
var settings = new PostIt.Settings var settings = new PostIt.Settings
@ -254,4 +254,4 @@ public class LoginPageViewModelTests
"https://yavsc.example.com/.well-known/openid-configuration", "https://yavsc.example.com/.well-known/openid-configuration",
vm.StatusMessage); vm.StatusMessage);
} }
} }

View file

@ -20,7 +20,7 @@ namespace PostIt.Tests;
/// the browser intercepts the authorize redirect, the server completes /// the browser intercepts the authorize redirect, the server completes
/// the token exchange. /// the token exchange.
/// </summary> /// </summary>
public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable public sealed class OIDCStubAuthority : IAsyncDisposable, IDisposable
{ {
private readonly HttpListener _listener; private readonly HttpListener _listener;
private readonly RSA _rsa; private readonly RSA _rsa;
@ -30,7 +30,7 @@ public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable
public string Issuer { get; } public string Issuer { get; }
public string LoopbackRedirectUri { get; } public string LoopbackRedirectUri { get; }
private OidcStubAuthority(HttpListener listener, RSA rsa, string kid, string issuer, string loopback) private OIDCStubAuthority(HttpListener listener, RSA rsa, string kid, string issuer, string loopback)
{ {
_listener = listener; _listener = listener;
_rsa = rsa; _rsa = rsa;
@ -39,7 +39,7 @@ public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable
LoopbackRedirectUri = loopback; LoopbackRedirectUri = loopback;
} }
public static async Task<OidcStubAuthority> StartAsync() public static async Task<OIDCStubAuthority> StartAsync()
{ {
// Pick a free loopback port. // Pick a free loopback port.
var port = GetFreePort(); var port = GetFreePort();
@ -53,7 +53,7 @@ public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable
var rsa = RSA.Create(2048); var rsa = RSA.Create(2048);
var kid = "test-key-1"; var kid = "test-key-1";
var authority = new OidcStubAuthority(listener, rsa, kid, prefix.TrimEnd('/'), loopback); var authority = new OIDCStubAuthority(listener, rsa, kid, prefix.TrimEnd('/'), loopback);
_ = Task.Run(() => authority.AcceptLoopAsync(authority._cts.Token)); _ = Task.Run(() => authority.AcceptLoopAsync(authority._cts.Token));
return authority; return authority;
} }

View file

@ -20,7 +20,7 @@ namespace PostIt.Tests;
/// End-to-end coverage of <see cref="YavscApiClient"/>: silent /// End-to-end coverage of <see cref="YavscApiClient"/>: silent
/// refresh on a near-expiry access token, 401-driven refresh + retry, /// refresh on a near-expiry access token, 401-driven refresh + retry,
/// and persistence of the token bundle via <see cref="TokenStore"/>. /// and persistence of the token bundle via <see cref="TokenStore"/>.
/// Uses the project's <see cref="OidcStubAuthority"/> for the IdP and /// Uses the project's <see cref="OIDCStubAuthority"/> for the IdP and
/// a tiny in-process HTTP listener for the API server side. /// a tiny in-process HTTP listener for the API server side.
/// </summary> /// </summary>
public class YavscApiClientTests public class YavscApiClientTests
@ -45,7 +45,7 @@ public class YavscApiClientTests
// in-memory access token as expired and re-run a call. The // in-memory access token as expired and re-run a call. The
// refresh path must rotate the refresh token transparently // refresh path must rotate the refresh token transparently
// and the API call must succeed with the new token. // and the API call must succeed with the new token.
using var authority = await OidcStubAuthority.StartAsync(); using var authority = await OIDCStubAuthority.StartAsync();
using var apiServer = new StubApiServer(); using var apiServer = new StubApiServer();
await apiServer.StartAsync(); await apiServer.StartAsync();
@ -63,7 +63,7 @@ public class YavscApiClientTests
var reloaded = new YavscApiClient(settings, new TokenStore(tokensPath)); var reloaded = new YavscApiClient(settings, new TokenStore(tokensPath));
var posts = await reloaded.CallAsync<List<StubApiServer.Post>>( var posts = await reloaded.CallAsync<List<StubApiServer.Post>>(
HttpMethod.Get, "posts"); HttpMethod.Get, "posts", TestContext.Current.CancellationToken);
Assert.NotNull(posts); Assert.NotNull(posts);
Assert.NotEmpty(posts); Assert.NotEmpty(posts);
@ -85,7 +85,7 @@ public class YavscApiClientTests
{ {
// API server returns 401 on the first request, 200 on the next. // API server returns 401 on the first request, 200 on the next.
// YavscApiClient must refresh, then retry exactly once. // YavscApiClient must refresh, then retry exactly once.
using var authority = await OidcStubAuthority.StartAsync(); using var authority = await OIDCStubAuthority.StartAsync();
using var apiServer = new StubApiServer(forceFirstRequest: true); using var apiServer = new StubApiServer(forceFirstRequest: true);
await apiServer.StartAsync(); await apiServer.StartAsync();
@ -97,7 +97,7 @@ public class YavscApiClientTests
settings, authority, tokensPath); settings, authority, tokensPath);
var posts = await client.CallAsync<List<StubApiServer.Post>>( var posts = await client.CallAsync<List<StubApiServer.Post>>(
HttpMethod.Get, "posts"); HttpMethod.Get, "posts", TestContext.Current.CancellationToken);
Assert.NotEmpty(posts); Assert.NotEmpty(posts);
Assert.Equal(2, apiServer.RequestCount); Assert.Equal(2, apiServer.RequestCount);
@ -125,14 +125,15 @@ public class YavscApiClientTests
var client = new YavscApiClient(settings, new TokenStore(Path.Combine( var client = new YavscApiClient(settings, new TokenStore(Path.Combine(
Path.GetTempPath(), $"postit-tests-noop-{Guid.NewGuid():N}.json"))); Path.GetTempPath(), $"postit-tests-noop-{Guid.NewGuid():N}.json")));
await Assert.ThrowsAsync<InvalidOperationException>(() => await Assert.ThrowsAsync<InvalidOperationException>(
client.CallAsync<JsonElement>(HttpMethod.Get, "posts")); () =>
client.CallAsync<JsonElement>(HttpMethod.Get, "posts", TestContext.Current.CancellationToken));
} }
[Fact] [Fact]
public async Task HasValidSession_is_true_after_login() public async Task HasValidSession_is_true_after_login()
{ {
using var authority = await OidcStubAuthority.StartAsync(); using var authority = await OIDCStubAuthority.StartAsync();
using var apiServer = new StubApiServer(); using var apiServer = new StubApiServer();
await apiServer.StartAsync(); await apiServer.StartAsync();
@ -154,7 +155,7 @@ public class YavscApiClientTests
// --- helpers -------------------------------------------------------- // --- helpers --------------------------------------------------------
private static PostIt.Settings BuildSettings(OidcStubAuthority authority, string apiBaseUrl) => new() private static PostIt.Settings BuildSettings(OIDCStubAuthority authority, string apiBaseUrl) => new()
{ {
Authentication = new AuthenticationSettings Authentication = new AuthenticationSettings
{ {
@ -167,7 +168,7 @@ public class YavscApiClientTests
}; };
private static async Task<YavscApiClient> LoginAndPersistAsync( private static async Task<YavscApiClient> LoginAndPersistAsync(
PostIt.Settings settings, OidcStubAuthority authority, string tokensPath) PostIt.Settings settings, OIDCStubAuthority authority, string tokensPath)
{ {
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri); var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
var client = new YavscApiClient(settings, new TokenStore(tokensPath)); var client = new YavscApiClient(settings, new TokenStore(tokensPath));
@ -181,7 +182,7 @@ public class YavscApiClientTests
/// <summary> /// <summary>
/// YavscApiClient.LoginInteractiveAsync delegates to /// YavscApiClient.LoginInteractiveAsync delegates to
/// Platform.CreateBrowser. We can't override that static cleanly /// Platform.CreateBrowser. We can't override that static cleanly
/// from xunit.v3, so we rebuild the call by re-routing the /// from XUnit.v3, so we rebuild the call by re-routing the
/// Platform.CreateBrowser delegate for the duration of the call. /// Platform.CreateBrowser delegate for the duration of the call.
/// </summary> /// </summary>
private static async Task LoginWithBrowserAsync( private static async Task LoginWithBrowserAsync(
@ -214,7 +215,7 @@ public class YavscApiClientTests
File.WriteAllText(tokensPath, JsonSerializer.Serialize(record)); File.WriteAllText(tokensPath, JsonSerializer.Serialize(record));
} }
// --- OidcLoginPhase progress tests --------------------------------- // --- OIDCLoginPhase progress tests ---------------------------------
/// <summary> /// <summary>
/// Collecting Progress<T> is documented to capture reports /// Collecting Progress<T> is documented to capture reports
@ -225,7 +226,7 @@ public class YavscApiClientTests
[Fact] [Fact]
public async Task LoginInteractiveAsync_reports_Discovering_then_Success() public async Task LoginInteractiveAsync_reports_Discovering_then_Success()
{ {
using var authority = await OidcStubAuthority.StartAsync(); using var authority = await OIDCStubAuthority.StartAsync();
using var apiServer = new StubApiServer(); using var apiServer = new StubApiServer();
await apiServer.StartAsync(); await apiServer.StartAsync();
@ -234,18 +235,18 @@ public class YavscApiClientTests
var client = new YavscApiClient(settings, new TokenStore(tokensPath)); var client = new YavscApiClient(settings, new TokenStore(tokensPath));
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri); var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
var reported = new System.Collections.Generic.List<OidcLoginPhase>(); var reported = new System.Collections.Generic.List<OIDCLoginPhase>();
var progress = new SyncProgress<OidcLoginPhase>(reported); var progress = new SyncProgress<OIDCLoginPhase>(reported);
try try
{ {
await LoginWithBrowserAsync(client, browser.CreateBrowser(), progress); await LoginWithBrowserAsync(client, browser.CreateBrowser(), progress);
// SyncProgress captures reports synchronously — no flush needed. // SyncProgress captures reports synchronously — no flush needed.
Assert.Contains(OidcLoginPhase.Discovering, reported); Assert.Contains(OIDCLoginPhase.Discovering, reported);
Assert.Contains(OidcLoginPhase.OpeningBrowser, reported); Assert.Contains(OIDCLoginPhase.OpeningBrowser, reported);
Assert.Contains(OidcLoginPhase.ExchangingCode, reported); Assert.Contains(OIDCLoginPhase.ExchangingCode, reported);
Assert.Equal(OidcLoginPhase.Success, Last(reported)); Assert.Equal(OIDCLoginPhase.Success, Last(reported));
} }
finally finally
{ {
@ -256,24 +257,24 @@ public class YavscApiClientTests
[Fact] [Fact]
public async Task LoginInteractiveAsync_reports_Error_when_browser_missing() public async Task LoginInteractiveAsync_reports_Error_when_browser_missing()
{ {
using var authority = await OidcStubAuthority.StartAsync(); using var authority = await OIDCStubAuthority.StartAsync();
using var apiServer = new StubApiServer(); using var apiServer = new StubApiServer();
await apiServer.StartAsync(); await apiServer.StartAsync();
var settings = BuildSettings(authority, apiServer.BaseUrl); var settings = BuildSettings(authority, apiServer.BaseUrl);
var client = new YavscApiClient(settings, new TokenStore(TokensPath())); var client = new YavscApiClient(settings, new TokenStore(TokensPath()));
var reported = new System.Collections.Generic.List<OidcLoginPhase>(); var reported = new System.Collections.Generic.List<OIDCLoginPhase>();
var progress = new SyncProgress<OidcLoginPhase>(reported); var progress = new SyncProgress<OIDCLoginPhase>(reported);
var original = Platform.CreateBrowser; var original = Platform.CreateBrowser;
try try
{ {
Platform.CreateBrowser = () => null; // simulate no browser wired up Platform.CreateBrowser = () => null; // simulate no browser wired up
await Assert.ThrowsAsync<InvalidOperationException>( await Assert.ThrowsAsync<InvalidOperationException>(
() => client.LoginInteractiveAsync(progress)); () => client.LoginInteractiveAsync(progress, TestContext.Current.CancellationToken));
// SyncProgress captures reports synchronously — no flush needed. // SyncProgress captures reports synchronously — no flush needed.
Assert.Equal(OidcLoginPhase.Error, Last(reported)); Assert.Equal(OIDCLoginPhase.Error, Last(reported));
} }
finally finally
{ {
@ -284,7 +285,7 @@ public class YavscApiClientTests
[Fact] [Fact]
public async Task TrySilentLoginAsync_returns_false_when_no_bundle_on_disk() public async Task TrySilentLoginAsync_returns_false_when_no_bundle_on_disk()
{ {
using var authority = await OidcStubAuthority.StartAsync(); using var authority = await OIDCStubAuthority.StartAsync();
using var apiServer = new StubApiServer(); using var apiServer = new StubApiServer();
await apiServer.StartAsync(); await apiServer.StartAsync();
@ -293,7 +294,7 @@ public class YavscApiClientTests
// Tokens file deliberately doesn't exist. // Tokens file deliberately doesn't exist.
var client = new YavscApiClient(settings, new TokenStore(tokensPath)); var client = new YavscApiClient(settings, new TokenStore(tokensPath));
var ok = await client.TrySilentLoginAsync(); var ok = await client.TrySilentLoginAsync(null, TestContext.Current.CancellationToken);
Assert.False(ok); Assert.False(ok);
Assert.False(client.HasValidSession); Assert.False(client.HasValidSession);
} }
@ -301,7 +302,7 @@ public class YavscApiClientTests
[Fact] [Fact]
public async Task TrySilentLoginAsync_returns_true_when_access_token_still_valid() public async Task TrySilentLoginAsync_returns_true_when_access_token_still_valid()
{ {
using var authority = await OidcStubAuthority.StartAsync(); using var authority = await OIDCStubAuthority.StartAsync();
using var apiServer = new StubApiServer(); using var apiServer = new StubApiServer();
await apiServer.StartAsync(); await apiServer.StartAsync();
@ -314,7 +315,7 @@ public class YavscApiClientTests
{ {
await LoginWithBrowserAsync(client, browser.CreateBrowser()); await LoginWithBrowserAsync(client, browser.CreateBrowser());
// Login fresh → access token is far from expiry. // Login fresh → access token is far from expiry.
var ok = await client.TrySilentLoginAsync(); var ok = await client.TrySilentLoginAsync(null, TestContext.Current.CancellationToken);
Assert.True(ok); Assert.True(ok);
Assert.True(client.HasValidSession); Assert.True(client.HasValidSession);
} }
@ -327,7 +328,7 @@ public class YavscApiClientTests
[Fact] [Fact]
public async Task TrySilentLoginAsync_returns_true_when_refresh_succeeds() public async Task TrySilentLoginAsync_returns_true_when_refresh_succeeds()
{ {
using var authority = await OidcStubAuthority.StartAsync(); using var authority = await OIDCStubAuthority.StartAsync();
using var apiServer = new StubApiServer(); using var apiServer = new StubApiServer();
await apiServer.StartAsync(); await apiServer.StartAsync();
@ -351,13 +352,13 @@ public class YavscApiClientTests
// matches the disk: access expired, refresh still good. // matches the disk: access expired, refresh still good.
var client = new YavscApiClient(settings, store); var client = new YavscApiClient(settings, store);
var reported = new System.Collections.Generic.List<OidcLoginPhase>(); var reported = new System.Collections.Generic.List<OIDCLoginPhase>();
var progress = new SyncProgress<OidcLoginPhase>(reported); var progress = new SyncProgress<OIDCLoginPhase>(reported);
var ok = await client.TrySilentLoginAsync(progress); var ok = await client.TrySilentLoginAsync(progress, TestContext.Current.CancellationToken);
Assert.True(ok, "silent refresh should succeed via the stub authority."); Assert.True(ok, "silent refresh should succeed via the stub authority.");
Assert.Contains(OidcLoginPhase.ExchangingCode, reported); Assert.Contains(OIDCLoginPhase.ExchangingCode, reported);
Assert.Equal(OidcLoginPhase.Success, Last(reported)); Assert.Equal(OIDCLoginPhase.Success, Last(reported));
} }
finally finally
{ {
@ -430,7 +431,7 @@ public class YavscApiClientTests
/// overload stays for tests that don't care about phase events. /// overload stays for tests that don't care about phase events.
/// </summary> /// </summary>
private static async Task LoginWithBrowserAsync( private static async Task LoginWithBrowserAsync(
YavscApiClient client, IBrowser browser, IProgress<OidcLoginPhase>? progress = null) YavscApiClient client, IBrowser browser, IProgress<OIDCLoginPhase>? progress = null)
{ {
var original = Platform.CreateBrowser; var original = Platform.CreateBrowser;
try try

View file

@ -12,7 +12,7 @@ namespace PostIt.Services;
/// The set is deliberately small: each value is a milestone an /// The set is deliberately small: each value is a milestone an
/// operator can grep for in logs / StatusMessage, not a heartbeat. /// operator can grep for in logs / StatusMessage, not a heartbeat.
/// </summary> /// </summary>
public enum OidcLoginPhase public enum OIDCLoginPhase
{ {
/// <summary>No login in flight (or login has settled).</summary> /// <summary>No login in flight (or login has settled).</summary>
Idle, Idle,

View file

@ -91,15 +91,15 @@ public class YavscApiClient : IAsyncDisposable
/// <see cref="LoginPageViewModel.StatusMessage"/> for the human /// <see cref="LoginPageViewModel.StatusMessage"/> for the human
/// text (URLs, error detail).</param> /// text (URLs, error detail).</param>
public async Task LoginInteractiveAsync( public async Task LoginInteractiveAsync(
IProgress<OidcLoginPhase>? progress = null, IProgress<OIDCLoginPhase>? progress = null,
CancellationToken ct = default) CancellationToken ct = default)
{ {
progress?.Report(OidcLoginPhase.Discovering); progress?.Report(OIDCLoginPhase.Discovering);
var browser = Platform.CreateBrowser?.Invoke(); var browser = Platform.CreateBrowser?.Invoke();
if (browser is null) if (browser is null)
{ {
progress?.Report(OidcLoginPhase.Error); progress?.Report(OIDCLoginPhase.Error);
throw new InvalidOperationException("No browser is available on this platform."); throw new InvalidOperationException("No browser is available on this platform.");
} }
@ -114,20 +114,20 @@ public class YavscApiClient : IAsyncDisposable
// the moment we ask the browser to open (covers the entire // the moment we ask the browser to open (covers the entire
// user-driven window including the AwaitingCallback wait), and // user-driven window including the AwaitingCallback wait), and
// the moment we trade the code for tokens. // the moment we trade the code for tokens.
progress?.Report(OidcLoginPhase.OpeningBrowser); progress?.Report(OIDCLoginPhase.OpeningBrowser);
var result = await client.LoginAsync(new LoginRequest(), ct).ConfigureAwait(false); var result = await client.LoginAsync(new LoginRequest(), ct).ConfigureAwait(false);
if (result.IsError) if (result.IsError)
{ {
progress?.Report(OidcLoginPhase.Error); progress?.Report(OIDCLoginPhase.Error);
throw new InvalidOperationException($"OIDC login failed: {result.Error}"); throw new InvalidOperationException($"OIDC login failed: {result.Error}");
} }
progress?.Report(OidcLoginPhase.ExchangingCode); progress?.Report(OIDCLoginPhase.ExchangingCode);
if (string.IsNullOrEmpty(result.RefreshToken)) if (string.IsNullOrEmpty(result.RefreshToken))
{ {
progress?.Report(OidcLoginPhase.Error); progress?.Report(OIDCLoginPhase.Error);
throw new InvalidOperationException( throw new InvalidOperationException(
"Missing refresh_token — vérifie le scope 'offline_access'."); "Missing refresh_token — vérifie le scope 'offline_access'.");
} }
@ -139,7 +139,7 @@ public class YavscApiClient : IAsyncDisposable
IdToken: result.IdentityToken); IdToken: result.IdentityToken);
_store.Save(_tokens); _store.Save(_tokens);
progress?.Report(OidcLoginPhase.Success); progress?.Report(OIDCLoginPhase.Success);
} }
/// <summary> /// <summary>
@ -152,7 +152,7 @@ public class YavscApiClient : IAsyncDisposable
/// phase and returns false so the UI can keep going. /// phase and returns false so the UI can keep going.
/// </summary> /// </summary>
public async Task<bool> TrySilentLoginAsync( public async Task<bool> TrySilentLoginAsync(
IProgress<OidcLoginPhase>? progress = null, IProgress<OIDCLoginPhase>? progress = null,
CancellationToken ct = default) CancellationToken ct = default)
{ {
if (!HasValidSession) return false; if (!HasValidSession) return false;
@ -161,7 +161,7 @@ public class YavscApiClient : IAsyncDisposable
// Access token still has plenty of life — nothing to do. // Access token still has plenty of life — nothing to do.
if (_tokens.AccessTokenExpiresAt - DateTimeOffset.UtcNow > RefreshSkew) if (_tokens.AccessTokenExpiresAt - DateTimeOffset.UtcNow > RefreshSkew)
{ {
progress?.Report(OidcLoginPhase.Success); progress?.Report(OIDCLoginPhase.Success);
return true; return true;
} }
@ -172,19 +172,19 @@ public class YavscApiClient : IAsyncDisposable
// the user back to the login page. // the user back to the login page.
try try
{ {
progress?.Report(OidcLoginPhase.ExchangingCode); progress?.Report(OIDCLoginPhase.ExchangingCode);
await ForceRefreshAsync(ct).ConfigureAwait(false); await ForceRefreshAsync(ct).ConfigureAwait(false);
progress?.Report(OidcLoginPhase.Success); progress?.Report(OIDCLoginPhase.Success);
return true; return true;
} }
catch (RefreshFailedException) catch (RefreshFailedException)
{ {
progress?.Report(OidcLoginPhase.Idle); progress?.Report(OIDCLoginPhase.Idle);
return false; return false;
} }
catch catch
{ {
progress?.Report(OidcLoginPhase.Idle); progress?.Report(OIDCLoginPhase.Idle);
return false; return false;
} }
} }

View file

@ -105,8 +105,8 @@ public partial class LoginPageViewModel : ViewModelBase
/// callback hand-off: when AwaitingCallback never resolves, /// callback hand-off: when AwaitingCallback never resolves,
/// the OS never re-launched PostIt with the postit:// URL. /// the OS never re-launched PostIt with the postit:// URL.
/// </summary> /// </summary>
private OidcLoginPhase _phase = OidcLoginPhase.Idle; private OIDCLoginPhase _phase = OIDCLoginPhase.Idle;
public OidcLoginPhase Phase public OIDCLoginPhase Phase
{ {
get => _phase; get => _phase;
private set private set
@ -122,13 +122,13 @@ public partial class LoginPageViewModel : ViewModelBase
/// </summary> /// </summary>
public string PhaseLabel => _phase switch public string PhaseLabel => _phase switch
{ {
OidcLoginPhase.Idle => "En attente", OIDCLoginPhase.Idle => "En attente",
OidcLoginPhase.Discovering => "Découverte OIDC…", OIDCLoginPhase.Discovering => "Découverte OIDC…",
OidcLoginPhase.OpeningBrowser => "Ouverture du navigateur…", OIDCLoginPhase.OpeningBrowser => "Ouverture du navigateur…",
OidcLoginPhase.AwaitingCallback => "En attente du callback postit://…", OIDCLoginPhase.AwaitingCallback => "En attente du callback postit://…",
OidcLoginPhase.ExchangingCode => "Échange du code contre les jetons…", OIDCLoginPhase.ExchangingCode => "Échange du code contre les jetons…",
OidcLoginPhase.Success => "Connecté", OIDCLoginPhase.Success => "Connecté",
OidcLoginPhase.Error => "Erreur", OIDCLoginPhase.Error => "Erreur",
_ => _phase.ToString(), _ => _phase.ToString(),
}; };
@ -275,7 +275,7 @@ public partial class LoginPageViewModel : ViewModelBase
// The progress sink drives Phase / PhaseLabel; StatusMessage // The progress sink drives Phase / PhaseLabel; StatusMessage
// keeps the text detail (URLs, error messages). Same // keeps the text detail (URLs, error messages). Same
// underlying flow, two views. // underlying flow, two views.
var progress = new Progress<OidcLoginPhase>(p => Phase = p); var progress = new Progress<OIDCLoginPhase>(p => Phase = p);
await LoginInteractiveCoreAsync(_api, progress); await LoginInteractiveCoreAsync(_api, progress);
IsBusy = false; IsBusy = false;
@ -299,7 +299,7 @@ public partial class LoginPageViewModel : ViewModelBase
/// </summary> /// </summary>
private async Task LoginInteractiveCoreAsync( private async Task LoginInteractiveCoreAsync(
YavscApiClient api, YavscApiClient api,
IProgress<OidcLoginPhase>? progress = null) IProgress<OIDCLoginPhase>? progress = null)
{ {
var original = Platform.CreateBrowser; var original = Platform.CreateBrowser;
try try

View file

@ -41,7 +41,7 @@ namespace Yavsc.Controllers
{ {
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
var now = DateTime.Now; var now = DateTime.Now;
var result = _context.RdvQueries.Include(c => c.Location). var result = _context.RdvQueries.Include(c => c.Location).
Include(c => c.Client).Where(c => c.PerformerId == uid && c.Id < maxId && c.EventDate > now Include(c => c.Client).Where(c => c.PerformerId == uid && c.Id < maxId && c.EventDate > now
&& c.ValidationDate == null). && c.ValidationDate == null).
@ -49,12 +49,12 @@ namespace Yavsc.Controllers
{ {
Client = new ClientProviderInfo { Client = new ClientProviderInfo {
UserName = c.Client.UserName, UserName = c.Client.UserName,
UserId = c.ClientId, UserId = c.ClientId,
Avatar = c.Client.Avatar }, Avatar = c.Client.Avatar },
Location = c.Location, Location = c.Location,
EventDate = c.EventDate, EventDate = c.EventDate,
Id = c.Id, Id = c.Id,
Previsional = c.Previsional, Previsional = c.Provisional,
Reason = c.Reason, Reason = c.Reason,
ActivityCode = c.ActivityCode, ActivityCode = c.ActivityCode,
BillingCode = BillingCodes.Rdv BillingCode = BillingCodes.Rdv

View file

@ -41,7 +41,7 @@ namespace Yavsc.ApiControllers
// user, as a client // user, as a client
public IActionResult Index() public IActionResult Index()
{ {
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
var now = DateTime.Now; var now = DateTime.Now;
@ -151,7 +151,7 @@ namespace Yavsc.ApiControllers
{ {
HairCutQuery query = await _context.HairCutQueries.Include(q => q.Client). HairCutQuery query = await _context.HairCutQueries.Include(q => q.Client).
Include(q => q.Client.PostalAddress).Include(q => q.Prestation).Include(q=>q.Regularisation) Include(q => q.Client.PostalAddress).Include(q => q.Prestation).Include(q=>q.Regularization)
.SingleAsync(q => q.Id == id); .SingleAsync(q => q.Id == id);
if (query.PaymentId!=null) if (query.PaymentId!=null)
return new BadRequestObjectResult(new { error = "An existing payment process already exists" }); return new BadRequestObjectResult(new { error = "An existing payment process already exists" });

View file

@ -37,7 +37,7 @@ namespace Yavsc
WorkflowHelpers.ConfigureBillingService(); WorkflowHelpers.ConfigureBillingService();
var firstRegistrar = new Func<ApplicationDbContext, long, IQuery>((db, id) => var firstRegistrar = new Func<ApplicationDbContext, long, IQuery>((db, id) =>
db.HairCutQueries.Include(q => q.Prestation).Include(q => q.Regularisation).Single(q => q.Id == id)); db.HairCutQueries.Include(q => q.Prestation).Include(q => q.Regularization).Single(q => q.Id == id));
const string testCode = "Brush"; const string testCode = "Brush";

View file

@ -17,7 +17,7 @@ namespace Yavsc.Controllers
// GET: GeneralSettings // GET: GeneralSettings
public async Task<IActionResult> Index() public async Task<IActionResult> Index()
{ {
return View(await _context.GeneralSettings.ToListAsync()); return View(await _context.MusicLoverSettings.ToListAsync());
} }
// GET: GeneralSettings/Details/5 // GET: GeneralSettings/Details/5
@ -28,7 +28,7 @@ namespace Yavsc.Controllers
return NotFound(); return NotFound();
} }
MusicLoverSettings generalSettings = await _context.GeneralSettings.SingleAsync(m => m.UserId == id); MusicLoverSettings generalSettings = await _context.MusicLoverSettings.SingleAsync(m => m.UserId == id);
if (generalSettings == null) if (generalSettings == null)
{ {
return NotFound(); return NotFound();
@ -50,7 +50,7 @@ namespace Yavsc.Controllers
{ {
if (ModelState.IsValid) if (ModelState.IsValid)
{ {
_context.GeneralSettings.Add(generalSettings); _context.MusicLoverSettings.Add(generalSettings);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
@ -65,7 +65,7 @@ namespace Yavsc.Controllers
return NotFound(); return NotFound();
} }
MusicLoverSettings generalSettings = await _context.GeneralSettings.SingleAsync(m => m.UserId == id); MusicLoverSettings generalSettings = await _context.MusicLoverSettings.SingleAsync(m => m.UserId == id);
if (generalSettings == null) if (generalSettings == null)
{ {
return NotFound(); return NotFound();
@ -96,7 +96,7 @@ namespace Yavsc.Controllers
return NotFound(); return NotFound();
} }
MusicLoverSettings generalSettings = await _context.GeneralSettings.SingleAsync(m => m.UserId == id); MusicLoverSettings generalSettings = await _context.MusicLoverSettings.SingleAsync(m => m.UserId == id);
if (generalSettings == null) if (generalSettings == null)
{ {
return NotFound(); return NotFound();
@ -110,8 +110,8 @@ namespace Yavsc.Controllers
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(string id) public async Task<IActionResult> DeleteConfirmed(string id)
{ {
MusicLoverSettings generalSettings = await _context.GeneralSettings.SingleAsync(m => m.UserId == id); MusicLoverSettings generalSettings = await _context.MusicLoverSettings.SingleAsync(m => m.UserId == id);
_context.GeneralSettings.Remove(generalSettings); _context.MusicLoverSettings.Remove(generalSettings);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return RedirectToAction("Index"); return RedirectToAction("Index");
} }

View file

@ -49,7 +49,7 @@ namespace Yavsc.Controllers
this.haircutLocalizer = haircutLocalizer; this.haircutLocalizer = haircutLocalizer;
} }
private async Task<HairCutQuery> GetQuery(long id) private async Task<HairCutQuery> GetQuery(long id)
{ {
var query = await _context.HairCutQueries var query = await _context.HairCutQueries
@ -58,7 +58,7 @@ namespace Yavsc.Controllers
.Include(x => x.Prestation) .Include(x => x.Prestation)
.Include(x => x.PerformerProfile.Performer) .Include(x => x.PerformerProfile.Performer)
.Include(x => x.PerformerProfile.Performer.DeviceDeclaration) .Include(x => x.PerformerProfile.Performer.DeviceDeclaration)
.Include(x => x.Regularisation) .Include(x => x.Regularization)
.SingleAsync(m => m.Id == id); .SingleAsync(m => m.Id == id);
query.SelectedProfile = await _context.BrusherProfile.SingleAsync(b => b.UserId == query.PerformerId); query.SelectedProfile = await _context.BrusherProfile.SingleAsync(b => b.UserId == query.PerformerId);
return query; return query;
@ -82,11 +82,11 @@ namespace Yavsc.Controllers
} }
var paymentInfo = await _context.ConfirmPayment(User.GetUserId(), PayerID, token); var paymentInfo = await _context.ConfirmPayment(User.GetUserId(), PayerID, token);
ViewBag.paymentinfo = paymentInfo; ViewBag.paymentinfo = paymentInfo;
command.Regularisation = paymentInfo.DbContent; command.Regularization = paymentInfo.DbContent;
command.PaymentId = token; command.PaymentId = token;
bool paymentOk = false; bool paymentOk = false;
if (paymentInfo.DetailsFromPayPal != null) if (paymentInfo.DetailsFromPayPal != null)
if (paymentInfo.DetailsFromPayPal.Ack == AckCodeType.SUCCESS) if (paymentInfo.DetailsFromPayPal.Ack == AckCodeType.SUCCESS)
{ {
// FIXME Assert (command.ValidationDate == null) // FIXME Assert (command.ValidationDate == null)
if (command.ValidationDate == null) { if (command.ValidationDate == null) {
@ -174,7 +174,7 @@ namespace Yavsc.Controllers
.Include(x => x.PerformerProfile) .Include(x => x.PerformerProfile)
.Include(x => x.Prestation) .Include(x => x.Prestation)
.Include(x => x.PerformerProfile.Performer) .Include(x => x.PerformerProfile.Performer)
.Include(x => x.Regularisation) .Include(x => x.Regularization)
.SingleOrDefaultAsync(m => m.Id == id); .SingleOrDefaultAsync(m => m.Id == id);
if (command == null) if (command == null)
{ {
@ -224,7 +224,7 @@ namespace Yavsc.Controllers
.FirstOrDefault( .FirstOrDefault(
x => x.PerformerId == model.PerformerId x => x.PerformerId == model.PerformerId
); );
if (taintIds != null) if (taintIds != null)
{ {

View file

@ -18,7 +18,7 @@ namespace Yavsc.Controllers
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
readonly IStringLocalizer<ProjectController> _localizer; readonly IStringLocalizer<ProjectController> _localizer;
readonly IStringLocalizer<BugController> _bugLocalizer; readonly IStringLocalizer<BugController> _bugLocalizer;
public ProjectController(ApplicationDbContext context, public ProjectController(ApplicationDbContext context,
IStringLocalizer<ProjectController> localizer, IStringLocalizer<ProjectController> localizer,
IStringLocalizer<BugController> bugLocalizer IStringLocalizer<BugController> bugLocalizer
@ -32,7 +32,7 @@ namespace Yavsc.Controllers
// GET: Project // GET: Project
public async Task<IActionResult> Index() public async Task<IActionResult> Index()
{ {
var applicationDbContext = _context.Project.Include(p => p.Client).Include(p => p.Context).Include(p => p.PerformerProfile).Include(p => p.Regularisation).Include(p => p.Repository); var applicationDbContext = _context.Project.Include(p => p.Client).Include(p => p.Context).Include(p => p.PerformerProfile).Include(p => p.Regularization).Include(p => p.Repository);
return View(await applicationDbContext.ToListAsync()); return View(await applicationDbContext.ToListAsync());
} }

View file

@ -4,14 +4,14 @@
<!-- Yavsc.Org-specific versions --> <!-- Yavsc.Org-specific versions -->
<ItemGroup> <ItemGroup>
<PackageVersion Include="AsciiDocSharp" Version="0.2.0" /> <PackageVersion Include="AsciiDocSharp" Version="0.1.0" />
<PackageVersion Include="AsciiDocSharp.Converters.Html" Version="0.2.0" /> <PackageVersion Include="AsciiDocSharp.Converters.Html" Version="0.1.0" />
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.6.2" /> <PackageVersion Include="BouncyCastle.Cryptography" Version="2.6.2" />
<PackageVersion Include="Google.Apis.Compute.v1" Version="1.74.0.4138" /> <PackageVersion Include="Google.Apis.Compute.v1" Version="1.74.0.4138" />
<PackageVersion Include="HigginsSoft.IdentityServer8.AspNetIdentity" Version="8.0.5-preview-net9" /> <PackageVersion Include="HigginsSoft.IdentityServer8.AspNetIdentity" Version="8.1.0-alpha.171" />
<PackageVersion Include="HigginsSoft.IdentityServer8.EntityFramework.Storage" Version="8.0.5-preview-net9" /> <PackageVersion Include="HigginsSoft.IdentityServer8.EntityFramework.Storage" Version="8.1.0-alpha.171" />
<PackageVersion Include="HigginsSoft.IdentityServer8.Security" Version="8.0.5-preview-net9" /> <PackageVersion Include="HigginsSoft.IdentityServer8.Security" Version="8.1.0-alpha.171" />
<PackageVersion Include="HigginsSoft.IdentityServer8.Storage" Version="8.0.5-preview-net9" /> <PackageVersion Include="HigginsSoft.IdentityServer8.Storage" Version="8.1.0-alpha.171" />
<PackageVersion Include="Microsoft.AspNetCore.Antiforgery" Version="2.3.11" /> <PackageVersion Include="Microsoft.AspNetCore.Antiforgery" Version="2.3.11" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.Google" Version="10.0.9" /> <PackageVersion Include="Microsoft.AspNetCore.Authentication.Google" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="10.0.9" /> <PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="10.0.9" />

View file

@ -17,11 +17,11 @@ namespace Yavsc.Helpers
{ {
Sender = query.ClientId, Sender = query.ClientId,
Reason = query.Reason, Reason = query.Reason,
Client = new ClientProviderInfo {  Client = new ClientProviderInfo {
UserName = query.Client.UserName , UserName = query.Client.UserName ,
UserId = query.ClientId, UserId = query.ClientId,
Avatar = query.Client.Avatar } , Avatar = query.Client.Avatar } ,
Previsional = query.Previsional, Previsional = query.Provisional,
EventDate = query.EventDate, EventDate = query.EventDate,
Location = query.Location, Location = query.Location,
Id = query.Id, Id = query.Id,
@ -44,7 +44,7 @@ namespace Yavsc.Helpers
var yaev = query.CreateEvent("NewHairCutQuery", var yaev = query.CreateEvent("NewHairCutQuery",
string.Format(SR["HairCutQueryValidation"],query.Client.UserName), string.Format(SR["HairCutQueryValidation"],query.Client.UserName),
$"{query.Client.Id}"); $"{query.Client.Id}");
return yaev; return yaev;
} }
@ -58,12 +58,12 @@ namespace Yavsc.Helpers
var yaev = new HairCutQueryEvent("newCommand") var yaev = new HairCutQueryEvent("newCommand")
{ {
Sender = query.ClientId, Sender = query.ClientId,
Client = new ClientProviderInfo {  Client = new ClientProviderInfo {
UserName = query.Client.UserName , UserName = query.Client.UserName ,
UserId = query.ClientId, UserId = query.ClientId,
Avatar = query.Client.Avatar } , Avatar = query.Client.Avatar } ,
Previsional = query.Previsional, Previsional = query.Provisional,
EventDate = query.EventDate, EventDate = query.EventDate,
Location = query.Location, Location = query.Location,
Id = query.Id, Id = query.Id,

View file

@ -54,7 +54,7 @@
<dt> @Html.DisplayNameFor(m => m.Location) <dt> @Html.DisplayNameFor(m => m.Location)
</dt> </dt>
<dd>@if (Model.Location == null) { <dd>@if (Model.Location == null) {
<p>Pas de lieu convenu ...</p> <p>Pas de lieu convenu ...</p>
} else { } else {
<label for="Location">Location</label> <label for="Location">Location</label>
@Html.DisplayFor(m => m.Location) @Html.DisplayFor(m => m.Location)
@ -62,7 +62,7 @@
</dd> </dd>
<dt>Notification <dt>Notification
</dt> </dt>
<dd>@if (ViewBag.GooglePayload !=null) <dd>@if (ViewBag.GooglePayload !=null)
{ {
@if (ViewBag.GooglePayload.success>0) { @if (ViewBag.GooglePayload.success>0) {
<h4>GCM Notifications sent</h4> <h4>GCM Notifications sent</h4>
@ -85,10 +85,10 @@
</dt> </dt>
<dd>@await Component.InvokeAsync("Bill", Model) <dd>@await Component.InvokeAsync("Bill", Model)
</dd> </dd>
<dt>@Html.DisplayNameFor(m=>m.Regularisation)</dt> <dt>@Html.DisplayNameFor(m=>m.Regularization)</dt>
<dd> @await Component.InvokeAsync("PayPalButton", Model) <dd> @await Component.InvokeAsync("PayPalButton", Model)
</dd> </dd>
</dl> </dl>
</div> </div>

View file

@ -21,9 +21,9 @@
<div class="form-group"> <div class="form-group">
<label asp-for="ActivityCode" class="col-md-2 control-label"></label> <label asp-for="ActivityCode" class="col-md-2 control-label"></label>
<div class="col-md-10"> <div class="col-md-10">
<select asp-for="ActivityCode" class ="form-control" asp-items="@ViewBag.ActivityCodeItems" ></select> <select asp-for="ActivityCode" class ="form-control" asp-items="@ViewBag.ActivityCodeItems" ></select>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="ClientId" class="col-md-2 control-label"></label> <label asp-for="ClientId" class="col-md-2 control-label"></label>
<div class="col-md-10"> <div class="col-md-10">
@ -57,12 +57,12 @@
<div class="col-md-10"> <div class="col-md-10">
<select asp-for="PerformerId" class ="form-control" asp-items="@ViewBag.PerformerIdItems"></select> <select asp-for="PerformerId" class ="form-control" asp-items="@ViewBag.PerformerIdItems"></select>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="Previsional" class="col-md-2 control-label"></label> <label asp-for="Provisional" class="col-md-2 control-label"></label>
<div class="col-md-10"> <div class="col-md-10">
<input asp-for="Previsional" class="form-control" /> <input asp-for="Provisional" class="form-control" />
<span asp-validation-for="Previsional" class="text-danger" ></span> <span asp-validation-for="Provisional" class="text-danger" ></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">

View file

@ -48,10 +48,10 @@
@Html.DisplayFor(model => model.OwnerId) @Html.DisplayFor(model => model.OwnerId)
</dd> </dd>
<dt> <dt>
@Html.DisplayNameFor(model => model.Previsional) @Html.DisplayNameFor(model => model.Provisional)
</dt> </dt>
<dd> <dd>
@Html.DisplayFor(model => model.Previsional) @Html.DisplayFor(model => model.Provisional)
</dd> </dd>
<dt> <dt>
@Html.DisplayNameFor(model => model.DateModified) @Html.DisplayNameFor(model => model.DateModified)

View file

@ -53,10 +53,10 @@
@Html.DisplayFor(model => model.OwnerId) @Html.DisplayFor(model => model.OwnerId)
</dd> </dd>
<dt> <dt>
@Html.DisplayNameFor(model => model.Previsional) @Html.DisplayNameFor(model => model.Provisional)
</dt> </dt>
<dd> <dd>
@Html.DisplayFor(model => model.Previsional) @Html.DisplayFor(model => model.Provisional)
</dd> </dd>
<dt> <dt>
@Html.DisplayNameFor(model => model.DateModified) @Html.DisplayNameFor(model => model.DateModified)

View file

@ -36,10 +36,10 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="Previsional" class="col-md-2 control-label">Prévisionel</label> <label asp-for="Provisional" class="col-md-2 control-label">Prévisionel</label>
<div class="col-md-10"> <div class="col-md-10">
<input asp-for="Previsional" class="form-control" /> <input asp-for="Provisional" class="form-control" />
<span asp-validation-for="Previsional" class="text-danger" ></span> <span asp-validation-for="Provisional" class="text-danger" ></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">

View file

@ -30,7 +30,7 @@
@Html.DisplayNameFor(model => model.OwnerId) @Html.DisplayNameFor(model => model.OwnerId)
</th> </th>
<th> <th>
@Html.DisplayNameFor(model => model.Previsional) @Html.DisplayNameFor(model => model.Provisional)
</th> </th>
<th> <th>
@Html.DisplayNameFor(model => model.DateModified) @Html.DisplayNameFor(model => model.DateModified)
@ -74,7 +74,7 @@
@Html.DisplayFor(modelItem => item.OwnerId) @Html.DisplayFor(modelItem => item.OwnerId)
</td> </td>
<td> <td>
@Html.DisplayFor(modelItem => item.Previsional) @Html.DisplayFor(modelItem => item.Provisional)
</td> </td>
<td> <td>
@Html.DisplayFor(modelItem => item.DateModified) @Html.DisplayFor(modelItem => item.DateModified)

View file

@ -1,25 +1,25 @@
@model NominativeServiceCommand @model NominativeServiceCommand
@if (Model!=null && Model.PaymentId!=null) { @if (Model!=null && Model.PaymentId!=null) {
@if (Model.Regularisation.Executor.Id == User.GetUserId()) { @if (Model.Regularization.Executor.Id == User.GetUserId()) {
<text> <text>
Votre paiment Votre paiment
</text> </text>
} else { } else {
<text> <text>
Le paiment de @Html.DisplayFor(m=>m.Regularisation.Executor.UserName) Le paiment de @Html.DisplayFor(m=>m.Regularization.Executor.UserName)
</text> </text>
} }
<text> : <text> :
</text> </text>
<label>@Model.GetIsAcquitted() <label>@Model.GetIsAcquitted()
<input type="checkbox" checked="@Model.GetIsAcquitted()" disabled/> <input type="checkbox" checked="@Model.GetIsAcquitted()" disabled/>
<a asp-controller="Manage" asp-action="PaymentInfo" asp-route-id="@Model.Regularisation.CreationToken">@Model.Regularisation.CreationToken</a> <a asp-controller="Manage" asp-action="PaymentInfo" asp-route-id="@Model.Regularization.CreationToken">@Model.Regularization.CreationToken</a>
</label> </label>
} }
@if (!Model.GetIsAcquitted()) { @if (!Model.GetIsAcquitted()) {
<div id="paypalzone"></div> <div id="paypalzone"></div>

View file

@ -24,8 +24,8 @@
</dt> </dt>
<dd>@await Component.InvokeAsync("Bill", Model) <dd>@await Component.InvokeAsync("Bill", Model)
</dd> </dd>
<dt>@Html.DisplayNameFor(m=>m.Regularisation)</dt> <dt>@Html.DisplayNameFor(m=>m.Regularization)</dt>
<dd> <dd>
@await Component.InvokeAsync("PayPalButton", Model) @await Component.InvokeAsync("PayPalButton", Model)
</dd> </dd>
</dl> </dl>

View file

@ -76,15 +76,21 @@ namespace Yavsc.Helpers
{ {
Config.ProfileTypes.Add(c); Config.ProfileTypes.Add(c);
} }
if (c.IsClass && !c.IsAbstract &&
c.GetInterface(nameof(IBillable)) != null)
{
BillingService.GlobalBillingMap.Add(c.Name, c.AssemblyQualifiedName);
}
} }
} }
foreach (var propertyInfo in typeof(ApplicationDbContext).GetProperties()) foreach (var propertyInfo in typeof(ApplicationDbContext).GetProperties())
{ {
foreach (var attr in propertyInfo.CustomAttributes) if (propertyInfo.PropertyType.IsGenericType &&
propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>))
{ {
// something like a DbSet? var entityType = propertyInfo.PropertyType.GetGenericArguments()[0];
if (typeof(Yavsc.Attributes.ActivitySettingsAttribute).IsAssignableFrom(attr.AttributeType)) if (typeof(IUserSettings).IsAssignableFrom(entityType))
{ {
BillingService.UserSettings.Add(propertyInfo); BillingService.UserSettings.Add(propertyInfo);
} }
@ -94,16 +100,16 @@ namespace Yavsc.Helpers
RegisterBilling<HairCutQuery>(BillingCodes.Brush, new Func<ApplicationDbContext, long, IQuery> RegisterBilling<HairCutQuery>(BillingCodes.Brush, new Func<ApplicationDbContext, long, IQuery>
((db, id) => ((db, id) =>
{ {
var query = db.HairCutQueries.Include(q => q.Prestation).Include(q => q.Regularisation).Single(q => q.Id == id); var query = db.HairCutQueries.Include(q => q.Prestation).Include(q => q.Regularization).Single(q => q.Id == id);
query.SelectedProfile = db.BrusherProfile.Single(b => b.UserId == query.PerformerId); query.SelectedProfile = db.BrusherProfile.Single(b => b.UserId == query.PerformerId);
return query; return query;
})); }));
RegisterBilling<HairMultiCutQuery>(BillingCodes.MBrush, new Func<ApplicationDbContext, long, IQuery> RegisterBilling<HairMultiCutQuery>(BillingCodes.MBrush, new Func<ApplicationDbContext, long, IQuery>
((db, id) => db.HairMultiCutQueries.Include(q => q.Regularisation).Single(q => q.Id == id))); ((db, id) => db.HairMultiCutQueries.Include(q => q.Regularization).Single(q => q.Id == id)));
RegisterBilling<RdvQuery>(BillingCodes.Rdv, new Func<ApplicationDbContext, long, IQuery> RegisterBilling<RdvQuery>(BillingCodes.Rdv, new Func<ApplicationDbContext, long, IQuery>
((db, id) => db.RdvQueries.Include(q => q.Regularisation).Single(q => q.Id == id))); ((db, id) => db.RdvQueries.Include(q => q.Regularization).Single(q => q.Id == id)));
} }
} }

View file

@ -290,22 +290,17 @@ namespace Yavsc.Models
public DbSet<Instrument> Instrument { get; set; } public DbSet<Instrument> Instrument { get; set; }
[ActivitySettings]
public DbSet<DjSettings> DjSettings { get; set; } public DbSet<DjSettings> DjSettings { get; set; }
[ActivitySettings]
public DbSet<Instrumentation> Instrumentation { get; set; } public DbSet<Instrumentation> Instrumentation { get; set; }
[ActivitySettings]
public DbSet<FormationSettings> FormationSettings { get; set; } public DbSet<FormationSettings> FormationSettings { get; set; }
[ActivitySettings] public DbSet<MusicLoverSettings> MusicLoverSettings { get; set; }
public DbSet<MusicLoverSettings> GeneralSettings { get; set; }
public DbSet<CoWorking> CoWorking { get; set; } public DbSet<CoWorking> CoWorking { get; set; }
private void AddTimestamps(string userId) private void AddTimestamps(string userId) 
{ { var entities =
var entities =
ChangeTracker.Entries() ChangeTracker.Entries()
.Where(x => x.Entity.GetType().GetInterface(nameof(ITrackedEntity)) != null .Where(x => x.Entity.GetType().GetInterface(nameof(ITrackedEntity)) != null
&& (x.State == EntityState.Added || x.State == EntityState.Modified)); && (x.State == EntityState.Added || x.State == EntityState.Modified));
@ -360,7 +355,6 @@ namespace Yavsc.Models
public DbSet<DismissClicked> DismissClicked { get; set; } public DbSet<DismissClicked> DismissClicked { get; set; }
[ActivitySettings]
public DbSet<BrusherProfile> BrusherProfile { get; set; } public DbSet<BrusherProfile> BrusherProfile { get; set; }
public DbSet<BankIdentity> BankIdentity { get; set; } public DbSet<BankIdentity> BankIdentity { get; set; }

View file

@ -59,14 +59,14 @@ namespace Yavsc.Models.Billing
/// <summary> /// <summary>
/// The performer identifier /// The performer identifier
/// </summary> /// </summary>
[ForeignKey("PerformerId"),Display(Name="Préstataire")] [ForeignKey("PerformerId"),Display(Name="PerformerProfile")]
public PerformerProfile PerformerProfile { get; set; } public PerformerProfile PerformerProfile { get; set; }
public DateTime? ValidationDate {get; set;} public DateTime? ValidationDate {get; set;}
[Display(Name="Previsional")] [Display(Name="Provisional")]
public decimal? Previsional { get; set; } public decimal? Provisional { get; set; }
/// <summary> /// <summary>
/// The bill /// The bill
/// </summary> /// </summary>
@ -82,7 +82,7 @@ namespace Yavsc.Models.Billing
public bool GetIsAcquitted() public bool GetIsAcquitted()
{ {
return Regularisation?.IsOk() ?? false; return Regularization?.IsOk() ?? false;
} }
public string GetFileBaseName(IBillingService billingService) public string GetFileBaseName(IBillingService billingService)
@ -93,11 +93,11 @@ namespace Yavsc.Models.Billing
return $"facture-{bcode}-{Id}{ack}"; return $"facture-{bcode}-{Id}{ack}";
} }
[ForeignKey("Regularisation")] [ForeignKey("Regularization")]
public string? PaymentId { get; set; } public string? PaymentId { get; set; }
[Display(Name = "Acquittement de la facture")] [Display(Name = "Acquittement de la facture")]
public virtual PayPalPayment Regularisation { get; set; } public virtual PayPalPayment Regularization { get; set; }
} }
} }

View file

@ -400,7 +400,7 @@ Prestation.Gender == HairCutGenders.Women ?
UserId = ClientId, UserId = ClientId,
Avatar = Client.Avatar Avatar = Client.Avatar
}, },
Previsional = Previsional, Previsional = Provisional,
EventDate = EventDate, EventDate = EventDate,
Location = Location, Location = Location,
Id = Id, Id = Id,

View file

@ -8,33 +8,20 @@ using Yavsc.Billing;
namespace Yavsc.Models.Haircut namespace Yavsc.Models.Haircut
{ {
public class HairPrestationCollectionItem {
[Key(), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public long PrestationId { get; set; }
[ForeignKeyAttribute("PrestationId")]
public virtual HairPrestation Prestation { get; set; }
public long QueryId { get; set; }
[ForeignKeyAttribute("QueryId")]
public virtual HairMultiCutQuery Query { get; set; }
}
public class HairMultiCutQuery : NominativeServiceCommand public class HairMultiCutQuery : NominativeServiceCommand
{ {
// Bill description // Bill description
string _customDescription = null; string _customDescription = null;
public override string Description public override string Description
{ {
get { get {
return _customDescription ?? "Prestation en coiffure à domicile [commande groupée]" ; return _customDescription ?? "Prestation en coiffure à domicile [commande groupée]" ;
} }
set { set {
_customDescription = value; _customDescription = value;
} }
} }
[Key(), DatabaseGenerated(DatabaseGeneratedOption.Identity)] [Key(), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
override public long Id { get; set; } override public long Id { get; set; }

View file

@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Yavsc.Models.Haircut
{
public class HairPrestationCollectionItem {
[Key(), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public long PrestationId { get; set; }
[ForeignKeyAttribute("PrestationId")]
public virtual HairPrestation Prestation { get; set; }
public long QueryId { get; set; }
[ForeignKeyAttribute("QueryId")]
public virtual HairMultiCutQuery Query { get; set; }
}
}

View file

@ -12,6 +12,9 @@ namespace Yavsc.Services
new Dictionary<string, Func<ApplicationDbContext, long, IQuery>>(); new Dictionary<string, Func<ApplicationDbContext, long, IQuery>>();
public static List<PropertyInfo> UserSettings = new List<PropertyInfo>(); public static List<PropertyInfo> UserSettings = new List<PropertyInfo>();
/// <summary>
/// Mapping from activity codes to IUserSettings
/// </summary>
public static Dictionary<string, string> GlobalBillingMap = public static Dictionary<string, string> GlobalBillingMap =
new Dictionary<string, string>(); new Dictionary<string, string>();
@ -30,7 +33,12 @@ namespace Yavsc.Services
return Task.FromResult(GetBillable(DbContext, billingCode, queryId)); return Task.FromResult(GetBillable(DbContext, billingCode, queryId));
} }
public static IQuery GetBillable(ApplicationDbContext context, string billingCode, long queryId) => Billing[billingCode](context, queryId); public static IQuery GetBillable(ApplicationDbContext context, string billingCode, long queryId)
{
throw new NotImplementedException();
}
public async Task<IUserSettings> GetPerformersSettingsAsync(string activityCode, string userId) public async Task<IUserSettings> GetPerformersSettingsAsync(string activityCode, string userId)
{ {
var activity = await DbContext.Activities.SingleAsync(a => a.Code == activityCode); var activity = await DbContext.Activities.SingleAsync(a => a.Code == activityCode);