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

@ -10,7 +10,7 @@ namespace PostIt.Tests;
/// URL emitted by OidcClient, extracts its <c>state</c>, and returns a
/// 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.
/// </summary>
public sealed class FakeAuthorizingBrowser

View file

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

View file

@ -20,7 +20,7 @@ namespace PostIt.Tests;
/// the browser intercepts the authorize redirect, the server completes
/// the token exchange.
/// </summary>
public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable
public sealed class OIDCStubAuthority : IAsyncDisposable, IDisposable
{
private readonly HttpListener _listener;
private readonly RSA _rsa;
@ -30,7 +30,7 @@ public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable
public string Issuer { 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;
_rsa = rsa;
@ -39,7 +39,7 @@ public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable
LoopbackRedirectUri = loopback;
}
public static async Task<OidcStubAuthority> StartAsync()
public static async Task<OIDCStubAuthority> StartAsync()
{
// Pick a free loopback port.
var port = GetFreePort();
@ -53,7 +53,7 @@ public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable
var rsa = RSA.Create(2048);
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));
return authority;
}

View file

@ -20,7 +20,7 @@ namespace PostIt.Tests;
/// End-to-end coverage of <see cref="YavscApiClient"/>: silent
/// refresh on a near-expiry access token, 401-driven refresh + retry,
/// 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.
/// </summary>
public class YavscApiClientTests
@ -45,7 +45,7 @@ public class YavscApiClientTests
// in-memory access token as expired and re-run a call. The
// refresh path must rotate the refresh token transparently
// 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();
await apiServer.StartAsync();
@ -63,7 +63,7 @@ public class YavscApiClientTests
var reloaded = new YavscApiClient(settings, new TokenStore(tokensPath));
var posts = await reloaded.CallAsync<List<StubApiServer.Post>>(
HttpMethod.Get, "posts");
HttpMethod.Get, "posts", TestContext.Current.CancellationToken);
Assert.NotNull(posts);
Assert.NotEmpty(posts);
@ -85,7 +85,7 @@ public class YavscApiClientTests
{
// API server returns 401 on the first request, 200 on the next.
// 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);
await apiServer.StartAsync();
@ -97,7 +97,7 @@ public class YavscApiClientTests
settings, authority, tokensPath);
var posts = await client.CallAsync<List<StubApiServer.Post>>(
HttpMethod.Get, "posts");
HttpMethod.Get, "posts", TestContext.Current.CancellationToken);
Assert.NotEmpty(posts);
Assert.Equal(2, apiServer.RequestCount);
@ -125,14 +125,15 @@ public class YavscApiClientTests
var client = new YavscApiClient(settings, new TokenStore(Path.Combine(
Path.GetTempPath(), $"postit-tests-noop-{Guid.NewGuid():N}.json")));
await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.CallAsync<JsonElement>(HttpMethod.Get, "posts"));
await Assert.ThrowsAsync<InvalidOperationException>(
() =>
client.CallAsync<JsonElement>(HttpMethod.Get, "posts", TestContext.Current.CancellationToken));
}
[Fact]
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();
await apiServer.StartAsync();
@ -154,7 +155,7 @@ public class YavscApiClientTests
// --- helpers --------------------------------------------------------
private static PostIt.Settings BuildSettings(OidcStubAuthority authority, string apiBaseUrl) => new()
private static PostIt.Settings BuildSettings(OIDCStubAuthority authority, string apiBaseUrl) => new()
{
Authentication = new AuthenticationSettings
{
@ -167,7 +168,7 @@ public class YavscApiClientTests
};
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 client = new YavscApiClient(settings, new TokenStore(tokensPath));
@ -181,7 +182,7 @@ public class YavscApiClientTests
/// <summary>
/// YavscApiClient.LoginInteractiveAsync delegates to
/// 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.
/// </summary>
private static async Task LoginWithBrowserAsync(
@ -214,7 +215,7 @@ public class YavscApiClientTests
File.WriteAllText(tokensPath, JsonSerializer.Serialize(record));
}
// --- OidcLoginPhase progress tests ---------------------------------
// --- OIDCLoginPhase progress tests ---------------------------------
/// <summary>
/// Collecting Progress<T> is documented to capture reports
@ -225,7 +226,7 @@ public class YavscApiClientTests
[Fact]
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();
await apiServer.StartAsync();
@ -234,18 +235,18 @@ public class YavscApiClientTests
var client = new YavscApiClient(settings, new TokenStore(tokensPath));
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
var reported = new System.Collections.Generic.List<OidcLoginPhase>();
var progress = new SyncProgress<OidcLoginPhase>(reported);
var reported = new System.Collections.Generic.List<OIDCLoginPhase>();
var progress = new SyncProgress<OIDCLoginPhase>(reported);
try
{
await LoginWithBrowserAsync(client, browser.CreateBrowser(), progress);
// SyncProgress captures reports synchronously — no flush needed.
Assert.Contains(OidcLoginPhase.Discovering, reported);
Assert.Contains(OidcLoginPhase.OpeningBrowser, reported);
Assert.Contains(OidcLoginPhase.ExchangingCode, reported);
Assert.Equal(OidcLoginPhase.Success, Last(reported));
Assert.Contains(OIDCLoginPhase.Discovering, reported);
Assert.Contains(OIDCLoginPhase.OpeningBrowser, reported);
Assert.Contains(OIDCLoginPhase.ExchangingCode, reported);
Assert.Equal(OIDCLoginPhase.Success, Last(reported));
}
finally
{
@ -256,24 +257,24 @@ public class YavscApiClientTests
[Fact]
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();
await apiServer.StartAsync();
var settings = BuildSettings(authority, apiServer.BaseUrl);
var client = new YavscApiClient(settings, new TokenStore(TokensPath()));
var reported = new System.Collections.Generic.List<OidcLoginPhase>();
var progress = new SyncProgress<OidcLoginPhase>(reported);
var reported = new System.Collections.Generic.List<OIDCLoginPhase>();
var progress = new SyncProgress<OIDCLoginPhase>(reported);
var original = Platform.CreateBrowser;
try
{
Platform.CreateBrowser = () => null; // simulate no browser wired up
await Assert.ThrowsAsync<InvalidOperationException>(
() => client.LoginInteractiveAsync(progress));
() => client.LoginInteractiveAsync(progress, TestContext.Current.CancellationToken));
// SyncProgress captures reports synchronously — no flush needed.
Assert.Equal(OidcLoginPhase.Error, Last(reported));
Assert.Equal(OIDCLoginPhase.Error, Last(reported));
}
finally
{
@ -284,7 +285,7 @@ public class YavscApiClientTests
[Fact]
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();
await apiServer.StartAsync();
@ -293,7 +294,7 @@ public class YavscApiClientTests
// Tokens file deliberately doesn't exist.
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(client.HasValidSession);
}
@ -301,7 +302,7 @@ public class YavscApiClientTests
[Fact]
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();
await apiServer.StartAsync();
@ -314,7 +315,7 @@ public class YavscApiClientTests
{
await LoginWithBrowserAsync(client, browser.CreateBrowser());
// 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(client.HasValidSession);
}
@ -327,7 +328,7 @@ public class YavscApiClientTests
[Fact]
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();
await apiServer.StartAsync();
@ -351,13 +352,13 @@ public class YavscApiClientTests
// matches the disk: access expired, refresh still good.
var client = new YavscApiClient(settings, store);
var reported = new System.Collections.Generic.List<OidcLoginPhase>();
var progress = new SyncProgress<OidcLoginPhase>(reported);
var reported = new System.Collections.Generic.List<OIDCLoginPhase>();
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.Contains(OidcLoginPhase.ExchangingCode, reported);
Assert.Equal(OidcLoginPhase.Success, Last(reported));
Assert.Contains(OIDCLoginPhase.ExchangingCode, reported);
Assert.Equal(OIDCLoginPhase.Success, Last(reported));
}
finally
{
@ -430,7 +431,7 @@ public class YavscApiClientTests
/// overload stays for tests that don't care about phase events.
/// </summary>
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;
try

View file

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

View file

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

View file

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

View file

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

View file

@ -41,7 +41,7 @@ namespace Yavsc.ApiControllers
// user, as a client
public IActionResult Index()
{
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
var now = DateTime.Now;
@ -151,7 +151,7 @@ namespace Yavsc.ApiControllers
{
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);
if (query.PaymentId!=null)
return new BadRequestObjectResult(new { error = "An existing payment process already exists" });

View file

@ -37,7 +37,7 @@ namespace Yavsc
WorkflowHelpers.ConfigureBillingService();
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";

View file

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

View file

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

View file

@ -18,7 +18,7 @@ namespace Yavsc.Controllers
private readonly ApplicationDbContext _context;
readonly IStringLocalizer<ProjectController> _localizer;
readonly IStringLocalizer<BugController> _bugLocalizer;
public ProjectController(ApplicationDbContext context,
IStringLocalizer<ProjectController> localizer,
IStringLocalizer<BugController> bugLocalizer
@ -32,7 +32,7 @@ namespace Yavsc.Controllers
// GET: Project
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());
}

View file

@ -4,14 +4,14 @@
<!-- Yavsc.Org-specific versions -->
<ItemGroup>
<PackageVersion Include="AsciiDocSharp" Version="0.2.0" />
<PackageVersion Include="AsciiDocSharp.Converters.Html" Version="0.2.0" />
<PackageVersion Include="AsciiDocSharp" Version="0.1.0" />
<PackageVersion Include="AsciiDocSharp.Converters.Html" Version="0.1.0" />
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.6.2" />
<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.EntityFramework.Storage" Version="8.0.5-preview-net9" />
<PackageVersion Include="HigginsSoft.IdentityServer8.Security" Version="8.0.5-preview-net9" />
<PackageVersion Include="HigginsSoft.IdentityServer8.Storage" 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.1.0-alpha.171" />
<PackageVersion Include="HigginsSoft.IdentityServer8.Security" Version="8.1.0-alpha.171" />
<PackageVersion Include="HigginsSoft.IdentityServer8.Storage" Version="8.1.0-alpha.171" />
<PackageVersion Include="Microsoft.AspNetCore.Antiforgery" Version="2.3.11" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.Google" 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,
Reason = query.Reason,
Client = new ClientProviderInfo { 
Client = new ClientProviderInfo {
UserName = query.Client.UserName ,
UserId = query.ClientId,
Avatar = query.Client.Avatar } ,
Previsional = query.Previsional,
Previsional = query.Provisional,
EventDate = query.EventDate,
Location = query.Location,
Id = query.Id,
@ -44,7 +44,7 @@ namespace Yavsc.Helpers
var yaev = query.CreateEvent("NewHairCutQuery",
string.Format(SR["HairCutQueryValidation"],query.Client.UserName),
$"{query.Client.Id}");
return yaev;
}
@ -58,12 +58,12 @@ namespace Yavsc.Helpers
var yaev = new HairCutQueryEvent("newCommand")
{
Sender = query.ClientId,
Client = new ClientProviderInfo { 
Client = new ClientProviderInfo {
UserName = query.Client.UserName ,
UserId = query.ClientId,
Avatar = query.Client.Avatar } ,
Previsional = query.Previsional,
Previsional = query.Provisional,
EventDate = query.EventDate,
Location = query.Location,
Id = query.Id,

View file

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

View file

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

View file

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

View file

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

View file

@ -36,10 +36,10 @@
</div>
</div>
<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">
<input asp-for="Previsional" class="form-control" />
<span asp-validation-for="Previsional" class="text-danger" ></span>
<input asp-for="Provisional" class="form-control" />
<span asp-validation-for="Provisional" class="text-danger" ></span>
</div>
</div>
<div class="form-group">

View file

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

View file

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

View file

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

View file

@ -76,15 +76,21 @@ namespace Yavsc.Helpers
{
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 attr in propertyInfo.CustomAttributes)
if (propertyInfo.PropertyType.IsGenericType &&
propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>))
{
// something like a DbSet?
if (typeof(Yavsc.Attributes.ActivitySettingsAttribute).IsAssignableFrom(attr.AttributeType))
var entityType = propertyInfo.PropertyType.GetGenericArguments()[0];
if (typeof(IUserSettings).IsAssignableFrom(entityType))
{
BillingService.UserSettings.Add(propertyInfo);
}
@ -94,16 +100,16 @@ namespace Yavsc.Helpers
RegisterBilling<HairCutQuery>(BillingCodes.Brush, new Func<ApplicationDbContext, long, IQuery>
((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);
return query;
}));
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>
((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; }
[ActivitySettings]
public DbSet<DjSettings> DjSettings { get; set; }
[ActivitySettings]
public DbSet<Instrumentation> Instrumentation { get; set; }
[ActivitySettings]
public DbSet<FormationSettings> FormationSettings { get; set; }
[ActivitySettings]
public DbSet<MusicLoverSettings> GeneralSettings { get; set; }
public DbSet<MusicLoverSettings> MusicLoverSettings { get; set; }
public DbSet<CoWorking> CoWorking { get; set; }
private void AddTimestamps(string userId)
{
var entities =
private void AddTimestamps(string userId) 
{ var entities =
ChangeTracker.Entries()
.Where(x => x.Entity.GetType().GetInterface(nameof(ITrackedEntity)) != null
&& (x.State == EntityState.Added || x.State == EntityState.Modified));
@ -360,7 +355,6 @@ namespace Yavsc.Models
public DbSet<DismissClicked> DismissClicked { get; set; }
[ActivitySettings]
public DbSet<BrusherProfile> BrusherProfile { get; set; }
public DbSet<BankIdentity> BankIdentity { get; set; }

View file

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

View file

@ -8,33 +8,20 @@ using Yavsc.Billing;
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
{
// Bill description
string _customDescription = null;
public override string Description
string _customDescription = null;
public override string Description
{
get {
get {
return _customDescription ?? "Prestation en coiffure à domicile [commande groupée]" ;
}
set {
_customDescription = value;
}
}
}
[Key(), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
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>>();
public static List<PropertyInfo> UserSettings = new List<PropertyInfo>();
/// <summary>
/// Mapping from activity codes to IUserSettings
/// </summary>
public static Dictionary<string, string> GlobalBillingMap =
new Dictionary<string, string>();
@ -30,7 +33,12 @@ namespace Yavsc.Services
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)
{
var activity = await DbContext.Activities.SingleAsync(a => a.Code == activityCode);