commit
aca3ceffe2
150 changed files with 7502 additions and 698 deletions
11
.vscode/mcp.json
vendored
Normal file
11
.vscode/mcp.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"servers": {
|
||||
"openclaw": {
|
||||
"type": "stdio",
|
||||
"command": "/home/paul/.nvm/versions/node/v22.23.0/bin/node",
|
||||
"args": [
|
||||
"/home/paul/Workspace/tools/openclaw-mcp-server.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
13
.vscode/settings.json
vendored
13
.vscode/settings.json
vendored
|
|
@ -26,5 +26,16 @@
|
|||
"cSpell.language": "fr,en",
|
||||
"makefile.configureOnOpen": false,
|
||||
"search.useGlobalIgnoreFiles": true,
|
||||
"search.useParentIgnoreFiles": true
|
||||
"search.useParentIgnoreFiles": true,
|
||||
"chat.mcp.serverSampling": {
|
||||
"yavsc/.vscode/mcp.json: openclaw": {
|
||||
"allowedModels": [
|
||||
"copilot/auto",
|
||||
"copilotcli/claude-haiku-4.5",
|
||||
"copilotcli/gpt-4.1",
|
||||
"copilotcli/gpt-5-mini",
|
||||
"copilotcli/mai-code-1-flash-picker"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@
|
|||
-->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
|
||||
<PackageVersion Include="GitVersion.MsBuild" Version="6.7.0" />
|
||||
<PackageVersion Include="HigginsSoft.IdentityServer8" Version="8.0.5-preview-net9" />
|
||||
<PackageVersion Include="HigginsSoft.IdentityServer8.EntityFramework" Version="8.0.5-preview-net9" />
|
||||
<PackageVersion Include="GitVersion.MsBuild" Version="6.8.1" />
|
||||
<PackageVersion Include="HigginsSoft.IdentityServer8" Version="8.1.0-alpha.171" />
|
||||
<PackageVersion Include="HigginsSoft.IdentityServer8.EntityFramework" Version="8.1.0-alpha.171" />
|
||||
<PackageVersion Include="IdentityModel.OidcClient" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" 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.InMemory" 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="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
||||
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ COPY src/PostIt/PostIt.Desktop/*.csproj ./src/PostIt/PostIt.Desktop/
|
|||
COPY . .
|
||||
|
||||
# 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
|
||||
RUN dotnet restore
|
||||
|
|
|
|||
6
Makefile
6
Makefile
|
|
@ -10,8 +10,8 @@ include .env
|
|||
all:
|
||||
dotnet build --nologo
|
||||
|
||||
clean:
|
||||
dotnet clean
|
||||
clean:
|
||||
dotnet clean -c $(CONFIG)
|
||||
|
||||
src/Yavsc/bin/output/wwwroot:
|
||||
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:
|
||||
dotnet build -p:Configuration=$(CONFIG) --project src/Yavsc.Org/Yavsc.Org.csproj
|
||||
|
||||
$(DESTDIR):
|
||||
$(DESTDIR):
|
||||
mkdir $(DESTDIR)
|
||||
|
||||
install: $(DESTDIR)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
188
src/PostIt.Tests/SignaturePadControlTests.cs
Normal file
188
src/PostIt.Tests/SignaturePadControlTests.cs
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using PostIt.Controls;
|
||||
using PostIt.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace PostIt.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Targeted tests for <see cref="SignaturePadControl"/> and
|
||||
/// <see cref="SignaturePadData"/>.
|
||||
///
|
||||
/// The control exposes <c>internal</c> test hooks so we can drive
|
||||
/// the buffer without standing up a headless XAML tree just to
|
||||
/// deliver synthetic pointer events. The headless surface is used
|
||||
/// only to assert that the control's pointer handlers are wired
|
||||
/// when a template is applied; see
|
||||
/// <see cref="Pointer_handlers_attach_when_capture_area_is_set"/>.
|
||||
/// </summary>
|
||||
public class SignaturePadControlTests
|
||||
{
|
||||
// --- SignaturePadData (pure) ---------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Data_empty_array_is_empty()
|
||||
{
|
||||
var d = new SignaturePadData(Array.Empty<int>());
|
||||
Assert.True(d.IsEmpty);
|
||||
Assert.Equal(0, d.StrokeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Data_single_dot_is_one_stroke_with_k_equals_one()
|
||||
{
|
||||
var d = new SignaturePadData(new[] { 1, 5_000, 5_000 });
|
||||
Assert.False(d.IsEmpty);
|
||||
Assert.Equal(1, d.StrokeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Data_two_strokes_are_independent()
|
||||
{
|
||||
var d = new SignaturePadData(new[]
|
||||
{
|
||||
2, 100, 100, 200, 200,
|
||||
1, 9_000, 9_000,
|
||||
});
|
||||
Assert.Equal(2, d.StrokeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Data_malformed_payload_does_not_throw_on_read()
|
||||
{
|
||||
// k=0 at the head would underflow the walker. The reader
|
||||
// short-circuits instead of throwing.
|
||||
var d = new SignaturePadData(new[] { 0, 1, 2, 3 });
|
||||
Assert.Equal(0, d.StrokeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Data_constructor_rejects_null()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new SignaturePadData(null!));
|
||||
}
|
||||
|
||||
// --- SignaturePadControl (buffer / events) -------------------------
|
||||
|
||||
[Fact]
|
||||
public void New_control_has_empty_buffer()
|
||||
{
|
||||
var pad = new SignaturePadControl();
|
||||
Assert.Empty(pad.Strokes);
|
||||
Assert.True(pad.Snapshot().IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Snapshot_returns_a_distinct_array_each_call()
|
||||
{
|
||||
var pad = new SignaturePadControl();
|
||||
pad.AppendPointForTest(1_000, 2_000);
|
||||
pad.AppendPointForTest(3_000, 4_000);
|
||||
pad.SealStrokeForTest();
|
||||
|
||||
var first = pad.Snapshot();
|
||||
var second = pad.Snapshot();
|
||||
|
||||
// Distinct array instances — the consumer of the first
|
||||
// snapshot can hold onto it after the control mutates.
|
||||
Assert.NotSame(first.Strokes, second.Strokes);
|
||||
// Same logical content (no mutation in between).
|
||||
Assert.Equal(first.Strokes, second.Strokes);
|
||||
|
||||
pad.AppendPointForTest(5_000, 6_000);
|
||||
pad.SealStrokeForTest();
|
||||
|
||||
var third = pad.Snapshot();
|
||||
Assert.NotEqual(first.Strokes, third.Strokes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_empties_buffer_and_raises_redraw()
|
||||
{
|
||||
var pad = new SignaturePadControl();
|
||||
pad.AppendPointForTest(1, 1);
|
||||
pad.SealStrokeForTest();
|
||||
Assert.NotEmpty(pad.Strokes);
|
||||
|
||||
int redraws = 0;
|
||||
pad.RedrawRequested += (_, _) => redraws++;
|
||||
pad.Clear();
|
||||
|
||||
Assert.Empty(pad.Strokes);
|
||||
Assert.True(pad.Snapshot().IsEmpty);
|
||||
Assert.Equal(1, redraws);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SealStrokeForTest_raises_redraw()
|
||||
{
|
||||
var pad = new SignaturePadControl();
|
||||
int redraws = 0;
|
||||
pad.RedrawRequested += (_, _) => redraws++;
|
||||
pad.AppendPointForTest(1, 1);
|
||||
pad.AppendPointForTest(2, 2);
|
||||
pad.SealStrokeForTest();
|
||||
Assert.Equal(1, redraws);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SealStrokeForTest_with_no_pending_points_is_a_no_op()
|
||||
{
|
||||
var pad = new SignaturePadControl();
|
||||
int redraws = 0;
|
||||
pad.RedrawRequested += (_, _) => redraws++;
|
||||
pad.SealStrokeForTest();
|
||||
Assert.Equal(0, redraws);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Two_sealed_strokes_produce_two_length_prefixes()
|
||||
{
|
||||
var pad = new SignaturePadControl();
|
||||
// Stroke 0: one point.
|
||||
pad.AppendPointForTest(1_000, 1_000);
|
||||
pad.SealStrokeForTest();
|
||||
// Stroke 1: two points.
|
||||
pad.AppendPointForTest(2_000, 2_000);
|
||||
pad.AppendPointForTest(3_000, 3_000);
|
||||
pad.SealStrokeForTest();
|
||||
|
||||
var s = pad.Strokes;
|
||||
// Layout: [k0, x0, y0, k1, x1, y1, x2, y2]
|
||||
Assert.Equal(1, s[0]);
|
||||
Assert.Equal(1_000, s[1]);
|
||||
Assert.Equal(1_000, s[2]);
|
||||
Assert.Equal(2, s[3]);
|
||||
Assert.Equal(2_000, s[4]);
|
||||
Assert.Equal(2_000, s[5]);
|
||||
Assert.Equal(3_000, s[6]);
|
||||
Assert.Equal(3_000, s[7]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StrokeCompleted_fires_on_seal()
|
||||
{
|
||||
var pad = new SignaturePadControl();
|
||||
int events = 0;
|
||||
pad.StrokeCompleted += (_, _) => events++;
|
||||
pad.AppendPointForTest(1, 1);
|
||||
pad.SealStrokeForTest();
|
||||
pad.AppendPointForTest(2, 2);
|
||||
pad.SealStrokeForTest();
|
||||
Assert.Equal(2, events);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StrokeCompleted_carries_a_snapshot_with_k_count()
|
||||
{
|
||||
var pad = new SignaturePadControl();
|
||||
SignaturePadData? captured = null;
|
||||
pad.StrokeCompleted += (_, d) => captured = d;
|
||||
pad.AppendPointForTest(1, 1);
|
||||
pad.AppendPointForTest(2, 2);
|
||||
pad.SealStrokeForTest();
|
||||
Assert.NotNull(captured);
|
||||
Assert.Equal(1, captured!.StrokeCount);
|
||||
}
|
||||
}
|
||||
163
src/PostIt.Tests/SignaturePageViewModelTests.cs
Normal file
163
src/PostIt.Tests/SignaturePageViewModelTests.cs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using PostIt.Controls;
|
||||
using PostIt.ViewModels;
|
||||
using Xunit;
|
||||
|
||||
namespace PostIt.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="SignaturePageViewModel"/>: the contract
|
||||
/// between the page's view model and the <see cref="SignaturePadControl"/>.
|
||||
/// The view (XAML + code-behind rendering) is not tested here — the
|
||||
/// control is render-agnostic, and the rendering is plain Polyline
|
||||
/// reconstruction that we'll exercise manually in PostIt.Desktop.
|
||||
/// </summary>
|
||||
public class SignaturePageViewModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Default_constructor_uses_default_dimensions()
|
||||
{
|
||||
var vm = new SignaturePageViewModel();
|
||||
Assert.Equal(SignaturePageViewModel.DefaultWidth, vm.Width);
|
||||
Assert.Equal(SignaturePageViewModel.DefaultHeight, vm.Height);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_rejects_non_positive_dimensions()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => new SignaturePageViewModel(0, 100));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => new SignaturePageViewModel(100, 0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => new SignaturePageViewModel(-1, 100));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Attach_then_Detach_is_idempotent()
|
||||
{
|
||||
var vm = new SignaturePageViewModel();
|
||||
var pad = new SignaturePadControl();
|
||||
vm.Attach(pad);
|
||||
vm.Detach();
|
||||
// Second detach is a no-op: must not throw.
|
||||
vm.Detach();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Attach_rejects_null()
|
||||
{
|
||||
var vm = new SignaturePageViewModel();
|
||||
Assert.Throws<ArgumentNullException>(() => vm.Attach(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StrokeCompleted_updates_status_and_counts()
|
||||
{
|
||||
var vm = new SignaturePageViewModel();
|
||||
var pad = new SignaturePadControl();
|
||||
vm.Attach(pad);
|
||||
|
||||
// Drive the control via the test hooks so we don't depend
|
||||
// on Avalonia pointer events.
|
||||
pad.AppendPointForTest(1_000, 1_000);
|
||||
pad.AppendPointForTest(2_000, 2_000);
|
||||
pad.SealStrokeForTest();
|
||||
|
||||
Assert.Equal(1, vm.StrokeCount);
|
||||
Assert.Equal(2, vm.PointCount);
|
||||
Assert.Contains("1 trait", vm.StatusMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_resets_counts_and_buffer()
|
||||
{
|
||||
var vm = new SignaturePageViewModel();
|
||||
var pad = new SignaturePadControl();
|
||||
vm.Attach(pad);
|
||||
|
||||
pad.AppendPointForTest(1, 1);
|
||||
pad.SealStrokeForTest();
|
||||
Assert.Equal(1, vm.StrokeCount);
|
||||
|
||||
vm.Clear();
|
||||
|
||||
Assert.Equal(0, vm.StrokeCount);
|
||||
Assert.Equal(0, vm.PointCount);
|
||||
Assert.Empty(pad.Strokes);
|
||||
Assert.Contains("Effacé", vm.StatusMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CaptureAsync_on_empty_buffer_reports_and_writes_nothing()
|
||||
{
|
||||
var vm = new SignaturePageViewModel();
|
||||
var pad = new SignaturePadControl();
|
||||
vm.Attach(pad);
|
||||
|
||||
await vm.CaptureAsync();
|
||||
|
||||
Assert.Contains("Rien", vm.StatusMessage);
|
||||
Assert.Null(vm.LastCapturedPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CaptureAsync_writes_a_yavsc_signature_v1_file()
|
||||
{
|
||||
// The VM uses Environment.SpecialFolder.LocalApplicationData,
|
||||
// which we cannot redirect per-call without a constructor
|
||||
// seam. We test the produced file's structure rather than
|
||||
// its text formatting, because System.Text.Json's pretty-
|
||||
// printer is not part of the contract we're locking down.
|
||||
var vm = new SignaturePageViewModel();
|
||||
var pad = new SignaturePadControl();
|
||||
vm.Attach(pad);
|
||||
|
||||
pad.AppendPointForTest(1_000, 2_000);
|
||||
pad.AppendPointForTest(3_000, 4_000);
|
||||
pad.SealStrokeForTest();
|
||||
|
||||
await vm.CaptureAsync();
|
||||
|
||||
Assert.NotNull(vm.LastCapturedPath);
|
||||
Assert.True(File.Exists(vm.LastCapturedPath!), $"file missing: {vm.LastCapturedPath}");
|
||||
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(vm.LastCapturedPath!));
|
||||
var root = doc.RootElement;
|
||||
|
||||
Assert.Equal("yavsc.signature/v1", root.GetProperty("format").GetString());
|
||||
Assert.Equal(10_000, root.GetProperty("coordinateMax").GetInt32());
|
||||
Assert.Equal(1, root.GetProperty("strokeCount").GetInt32());
|
||||
|
||||
var strokes = root.GetProperty("strokes");
|
||||
Assert.Equal(JsonValueKind.Array, strokes.ValueKind);
|
||||
// [k=2, x0, y0, x1, y1]
|
||||
Assert.Equal(5, strokes.GetArrayLength());
|
||||
Assert.Equal(2, strokes[0].GetInt32()); // k (2 points)
|
||||
Assert.Equal(1_000, strokes[1].GetInt32()); // x0
|
||||
Assert.Equal(2_000, strokes[2].GetInt32()); // y0
|
||||
Assert.Equal(3_000, strokes[3].GetInt32()); // x1
|
||||
Assert.Equal(4_000, strokes[4].GetInt32()); // y1
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CaptureAsync_creates_directory_if_missing()
|
||||
{
|
||||
var vm = new SignaturePageViewModel();
|
||||
var pad = new SignaturePadControl();
|
||||
vm.Attach(pad);
|
||||
pad.AppendPointForTest(1, 1);
|
||||
pad.SealStrokeForTest();
|
||||
|
||||
// The directory must exist after the call (CreateDirectory
|
||||
// in the VM handles this).
|
||||
await vm.CaptureAsync();
|
||||
|
||||
var dir = Path.GetDirectoryName(vm.LastCapturedPath!);
|
||||
Assert.NotNull(dir);
|
||||
Assert.True(Directory.Exists(dir), $"directory missing: {dir}");
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ public partial class App : Application
|
|||
services.AddTransient<LoginPage>();
|
||||
services.AddTransient<SettingsPage>();
|
||||
services.AddTransient<HomePage>();
|
||||
services.AddTransient<SignaturePage>();
|
||||
|
||||
// ViewModels
|
||||
services.AddSingleton(settings);
|
||||
|
|
@ -72,6 +73,7 @@ public partial class App : Application
|
|||
services.AddTransient<SettingsPageViewModel>();
|
||||
services.AddTransient<LoginPageViewModel>();
|
||||
services.AddTransient<HomePageViewModel>();
|
||||
services.AddTransient<SignaturePageViewModel>();
|
||||
|
||||
// Persistent session banner: one instance for the lifetime of
|
||||
// the app so the same VM survives page navigation.
|
||||
|
|
|
|||
204
src/PostIt/PostIt/Controls/SignaturePadControl.cs
Normal file
204
src/PostIt/PostIt/Controls/SignaturePadControl.cs
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using PostIt.Models;
|
||||
|
||||
namespace PostIt.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Pointer-driven capture surface that records a signature as a list
|
||||
/// of strokes, each stroke being a length-prefixed sequence of (x, y)
|
||||
/// coordinates normalised to <c>[0, CoordinateMax]</c>.
|
||||
///
|
||||
/// The control is render-agnostic: it does not draw anything. The
|
||||
/// host view templates a <see cref="InputElement"/> (typically a
|
||||
/// <c>Border</c>) as <c>PART_CaptureArea</c> for pointer capture,
|
||||
/// and binds a separate visual layer (e.g. a <c>Canvas</c>) to
|
||||
/// <see cref="Strokes"/> for redraw. Keeping the control headless of
|
||||
/// rendering makes it usable from a headless test where no
|
||||
/// composition happens.
|
||||
///
|
||||
/// Wire format (see <see cref="SignaturePadData"/>):
|
||||
/// <code>int[] = [k0, x0, y0, ..., k1, x0, y0, ...]</code>
|
||||
/// with <c>x, y ∈ [0, 10_000]</c>.
|
||||
///
|
||||
/// Threading: pointer events are dispatched on the UI thread, which
|
||||
/// is the only thread that ever mutates <see cref="Strokes"/>. The
|
||||
/// buffer is safe to read from any thread as long as no read
|
||||
/// straddles a pointer event — for cross-thread transfer use
|
||||
/// <see cref="Snapshot"/>, which copies.
|
||||
/// </summary>
|
||||
public class SignaturePadControl : TemplatedControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Styled property pointing at the <see cref="InputElement"/>
|
||||
/// that receives pointer events. Set it in the control's
|
||||
/// template (<c>PART_CaptureArea</c>).
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<InputElement?> CaptureAreaProperty =
|
||||
AvaloniaProperty.Register<SignaturePadControl, InputElement?>(nameof(CaptureArea));
|
||||
|
||||
public InputElement? CaptureArea
|
||||
{
|
||||
get => GetValue(CaptureAreaProperty);
|
||||
set => SetValue(CaptureAreaProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captured strokes in wire form. Exposed as a read-only view
|
||||
/// over the internal buffer. The buffer only mutates on the UI
|
||||
/// thread, between pointer events.
|
||||
/// </summary>
|
||||
public IReadOnlyList<int> Strokes => _strokes;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the user finishes a stroke (pointer release).
|
||||
/// The argument is a snapshot of the buffer at release time.
|
||||
/// </summary>
|
||||
public event EventHandler<SignaturePadData>? StrokeCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the buffer changes: at the end of every stroke
|
||||
/// and on <see cref="Clear"/>. Mid-stroke points do not raise
|
||||
/// this event (pointer-move is too dense); bind a separate
|
||||
/// visual layer if you need a live preview.
|
||||
/// </summary>
|
||||
public event EventHandler? RedrawRequested;
|
||||
|
||||
private readonly List<int> _strokes = new(capacity: 256);
|
||||
private int _pendingPoints; // number of (x, y) pairs awaiting a length prefix
|
||||
private bool _capturing;
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
|
||||
if (CaptureArea is { } previous)
|
||||
{
|
||||
previous.PointerPressed -= OnCapturePressed;
|
||||
previous.PointerMoved -= OnCaptureMoved;
|
||||
previous.PointerReleased -= OnCaptureReleased;
|
||||
}
|
||||
|
||||
if (CaptureArea is { } area)
|
||||
{
|
||||
area.PointerPressed += OnCapturePressed;
|
||||
area.PointerMoved += OnCaptureMoved;
|
||||
area.PointerReleased += OnCaptureReleased;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCapturePressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (!e.GetCurrentPoint(CaptureArea).Properties.IsLeftButtonPressed) return;
|
||||
e.Pointer.Capture(CaptureArea);
|
||||
_capturing = true;
|
||||
_pendingPoints = 0;
|
||||
AppendPoint(e.GetPosition(CaptureArea));
|
||||
}
|
||||
|
||||
private void OnCaptureMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (!_capturing) return;
|
||||
AppendPoint(e.GetPosition(CaptureArea));
|
||||
}
|
||||
|
||||
private void OnCaptureReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (!_capturing) return;
|
||||
AppendPoint(e.GetPosition(CaptureArea));
|
||||
_capturing = false;
|
||||
|
||||
if (_pendingPoints == 0)
|
||||
{
|
||||
// Press + immediate release without movement yields no
|
||||
// point at all (the press fired AppendPoint, so this
|
||||
// branch is unreachable — kept for clarity if a future
|
||||
// change skips the press append).
|
||||
return;
|
||||
}
|
||||
|
||||
// Seal the current stroke by inserting its length at the
|
||||
// head of its slice. The slice is the trailing
|
||||
// 2 * _pendingPoints entries.
|
||||
int sliceStart = _strokes.Count - 2 * _pendingPoints;
|
||||
_strokes.Insert(sliceStart, _pendingPoints);
|
||||
_pendingPoints = 0;
|
||||
|
||||
StrokeCompleted?.Invoke(this, Snapshot());
|
||||
RedrawRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void AppendPoint(Point p)
|
||||
{
|
||||
var (nx, ny) = Normalise(p);
|
||||
_strokes.Add(nx);
|
||||
_strokes.Add(ny);
|
||||
_pendingPoints++;
|
||||
}
|
||||
|
||||
private (int x, int y) Normalise(Point p)
|
||||
{
|
||||
if (CaptureArea is null) return (0, 0);
|
||||
var bounds = CaptureArea.Bounds;
|
||||
double w = bounds.Width;
|
||||
double h = bounds.Height;
|
||||
if (w <= 0 || h <= 0) return (0, 0);
|
||||
int nx = (int)Math.Round(Math.Clamp(p.X / w, 0.0, 1.0) * SignaturePadData.CoordinateMax);
|
||||
int ny = (int)Math.Round(Math.Clamp(p.Y / h, 0.0, 1.0) * SignaturePadData.CoordinateMax);
|
||||
return (nx, ny);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forget every captured stroke. Raises <see cref="RedrawRequested"/>.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_strokes.Clear();
|
||||
_pendingPoints = 0;
|
||||
_capturing = false;
|
||||
RedrawRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defensive copy of the current buffer wrapped in a
|
||||
/// <see cref="SignaturePadData"/>. Cheap; call only when the
|
||||
/// view needs to ship the data off (e.g. to a backend).
|
||||
/// </summary>
|
||||
public SignaturePadData Snapshot() => new(_strokes.ToArray());
|
||||
|
||||
// --- Test-only surface (visible to PostIt.Tests) -------------------
|
||||
|
||||
/// <summary>
|
||||
/// Test hook: append a single normalised point without going
|
||||
/// through the pointer pipeline. Does not raise
|
||||
/// <see cref="RedrawRequested"/>.
|
||||
/// </summary>
|
||||
internal void AppendPointForTest(int x, int y)
|
||||
{
|
||||
_strokes.Add(x);
|
||||
_strokes.Add(y);
|
||||
_pendingPoints++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test hook: seal the currently-pending stroke with a length
|
||||
/// prefix. Mirrors what <see cref="OnCaptureReleased"/> does at
|
||||
/// pointer release time, including the
|
||||
/// <see cref="StrokeCompleted"/> and <see cref="RedrawRequested"/>
|
||||
/// events, so test scenarios observe the same notification
|
||||
/// contract as production. Idempotent: a second call without
|
||||
/// intermediate appends is a no-op.
|
||||
/// </summary>
|
||||
internal void SealStrokeForTest()
|
||||
{
|
||||
if (_pendingPoints == 0) return;
|
||||
int sliceStart = _strokes.Count - 2 * _pendingPoints;
|
||||
_strokes.Insert(sliceStart, _pendingPoints);
|
||||
_pendingPoints = 0;
|
||||
StrokeCompleted?.Invoke(this, Snapshot());
|
||||
RedrawRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
103
src/PostIt/PostIt/Models/SignaturePadData.cs
Normal file
103
src/PostIt/PostIt/Models/SignaturePadData.cs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
namespace PostIt.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Serialized form of a signature captured by
|
||||
/// <see cref="PostIt.Controls.SignaturePadControl"/>.
|
||||
///
|
||||
/// Wire format (length-prefixed, normalised):
|
||||
/// <code>
|
||||
/// int[] = [k0, x00, y00, x01, y01, ..., x0_{k0-1}, y0_{k0-1},
|
||||
/// k1, x10, y10, x11, y11, ..., x1_{k1-1}, y1_{k1-1},
|
||||
/// ...]
|
||||
/// </code>
|
||||
/// <list type="bullet">
|
||||
/// <item><c>k_i</c> — number of (x, y) pairs in stroke <c>i</c>.</item>
|
||||
/// <item><c>x, y</c> — coordinates normalised to <c>[0, CoordinateMax]</c>
|
||||
/// (inclusive) on the control's client area. <see cref="CoordinateMax"/>
|
||||
/// is <c>10_000</c> by default — a 4-decimal fixed-point fraction of
|
||||
/// the surface, which is enough to discriminate 0.01% of the diagonal
|
||||
/// on any reasonable screen and stays well inside <c>int</c>.</item>
|
||||
/// <item>Total array length is even: each stroke contributes
|
||||
/// <c>1 + 2 * k_i</c> integers, and <c>1 + 2k</c> is always odd.
|
||||
/// Sum of <c>1 + 2k_i</c> over strokes is therefore odd * N, which
|
||||
/// is odd when N is odd and even when N is even — so the overall
|
||||
/// "size pair" property is not enforced, only the per-stroke shape
|
||||
/// is. If the consumer needs a strictly even total, pad the last
|
||||
/// stroke with a duplicate terminal point (or use
|
||||
/// <see cref="IsEmpty"/> to drop the array entirely).</item>
|
||||
/// </list>
|
||||
///
|
||||
/// Empty signature (no strokes) is represented by an empty array
|
||||
/// (length 0). A single dot — pen down + pen up at the same point —
|
||||
/// is a single stroke with <c>k = 1</c>: <c>[1, x, y]</c>.
|
||||
/// </summary>
|
||||
public sealed class SignaturePadData
|
||||
{
|
||||
/// <summary>
|
||||
/// Upper bound of normalised coordinates. <c>10_000</c> means a
|
||||
/// surface unit is represented as 0.0001 of the whole.
|
||||
/// </summary>
|
||||
public const int CoordinateMax = 10_000;
|
||||
|
||||
/// <summary>
|
||||
/// Raw payload. See <see cref="SignaturePadData"/> for the layout.
|
||||
/// Never <c>null</c>; an empty array means "no strokes".
|
||||
/// </summary>
|
||||
public int[] Strokes { get; }
|
||||
|
||||
public SignaturePadData(int[] strokes)
|
||||
{
|
||||
if (strokes is null) throw new System.ArgumentNullException(nameof(strokes));
|
||||
Strokes = strokes;
|
||||
}
|
||||
|
||||
/// <summary>True if no stroke has been captured.</summary>
|
||||
public bool IsEmpty => Strokes.Length == 0;
|
||||
|
||||
/// <summary>
|
||||
/// Number of distinct strokes (pen-down / pen-up cycles).
|
||||
/// Returns 0 when <see cref="IsEmpty"/> is true.
|
||||
/// </summary>
|
||||
public int StrokeCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Strokes.Length == 0) return 0;
|
||||
int n = 0;
|
||||
int i = 0;
|
||||
while (i < Strokes.Length)
|
||||
{
|
||||
int k = Strokes[i];
|
||||
// Defensive: a malformed entry is treated as 0 so we
|
||||
// never throw on read. The capture side never produces
|
||||
// these, this is only for robustness on the wire.
|
||||
if (k <= 0) return n;
|
||||
i += 1 + 2 * k;
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Total number of (x, y) pairs across all strokes. Useful
|
||||
/// for sanity-checks and for displaying capture density
|
||||
/// without re-walking the wire format.
|
||||
/// </summary>
|
||||
public int PointCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int n = 0;
|
||||
int i = 0;
|
||||
while (i < Strokes.Length)
|
||||
{
|
||||
int k = Strokes[i];
|
||||
if (k <= 0) break;
|
||||
n += k;
|
||||
i += 1 + 2 * k;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -205,6 +205,16 @@ public class YavscApiClient : IAsyncDisposable
|
|||
return dto!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call a JSON endpoint with no request body while still allowing a
|
||||
/// positional cancellation token argument.
|
||||
/// </summary>
|
||||
public Task<T> CallAsync<T>(
|
||||
HttpMethod method,
|
||||
string path,
|
||||
CancellationToken ct)
|
||||
=> CallAsync<T>(method, path, body: null, ct);
|
||||
|
||||
/// <summary>Call an endpoint that returns no useful body (DELETE, etc.).</summary>
|
||||
public async Task CallAsync(
|
||||
HttpMethod method,
|
||||
|
|
@ -216,6 +226,16 @@ public class YavscApiClient : IAsyncDisposable
|
|||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call an endpoint with no request body while still allowing a
|
||||
/// positional cancellation token argument.
|
||||
/// </summary>
|
||||
public Task CallAsync(
|
||||
HttpMethod method,
|
||||
string path,
|
||||
CancellationToken ct)
|
||||
=> CallAsync(method, path, body: null, ct);
|
||||
|
||||
private async Task<HttpResponseMessage> SendAsync(
|
||||
HttpMethod method, string path, object? body, CancellationToken ct)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ public class ViewLocator : IDataTemplate
|
|||
SettingsPageViewModel => _services.GetRequiredService<SettingsPage>(),
|
||||
LoginPageViewModel => _services.GetRequiredService<LoginPage>(),
|
||||
HomePageViewModel => _services.GetRequiredService<HomePage>(),
|
||||
SignaturePageViewModel => _services.GetRequiredService<SignaturePage>(),
|
||||
_ => new TextBlock { Text = $"No view for {data.GetType().Name}" }
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
185
src/PostIt/PostIt/ViewModels/SignaturePageViewModel.cs
Normal file
185
src/PostIt/PostIt/ViewModels/SignaturePageViewModel.cs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using PostIt.Controls;
|
||||
using PostIt.Models;
|
||||
|
||||
namespace PostIt.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Backing state for <see cref="PostIt.Views.SignaturePage"/>.
|
||||
///
|
||||
/// The page exists to produce a <see cref="SignaturePadData"/>
|
||||
/// (length-prefixed normalised int[]) from a human signature drawn
|
||||
/// with the mouse (Desktop) or finger (touch / Android). The page
|
||||
/// is a recipient of an external trigger — a SignalR push from
|
||||
/// Yavsc.Org telling PostIt "a devis has been sent, sign here" —
|
||||
/// so it intentionally has no first-class entry point in
|
||||
/// <see cref="MainPage"/>. The only "open" affordance today is a
|
||||
/// dev-only shortcut on the blog editor, marked for removal once
|
||||
/// the SignalR handler lands.
|
||||
///
|
||||
/// Output path is the platform-friendly per-user data directory
|
||||
/// (XDG_DATA_HOME / AppData / NSDocumentDirectory on iOS). Files
|
||||
/// are JSON, one per capture, named
|
||||
/// <c>signature-{yyyyMMdd-HHmmssfff}.json</c>. This is a stop-gap
|
||||
/// until the Yavsc.Org endpoint exists; the contract there will
|
||||
/// be <c>POST /api/signature/{devisId}</c> with this same payload.
|
||||
/// </summary>
|
||||
public partial class SignaturePageViewModel : ViewModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Default capture surface, in DIPs. 3:1 ratio matches a
|
||||
/// signature line at the bottom of an A4 contract.
|
||||
/// </summary>
|
||||
public const double DefaultWidth = 600;
|
||||
public const double DefaultHeight = 200;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string StatusMessage { get; set; } = "Prêt.";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int StrokeCount { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int PointCount { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string? LastCapturedPath { get; set; }
|
||||
|
||||
public double Width { get; }
|
||||
public double Height { get; }
|
||||
|
||||
private SignaturePadControl? _control;
|
||||
|
||||
public override bool CanNavigateNext
|
||||
{
|
||||
get => false;
|
||||
protected set { _ = value; }
|
||||
}
|
||||
|
||||
public override bool CanNavigatePrevious
|
||||
{
|
||||
get => true;
|
||||
protected set { _ = value; }
|
||||
}
|
||||
|
||||
public SignaturePageViewModel()
|
||||
: this(DefaultWidth, DefaultHeight)
|
||||
{
|
||||
}
|
||||
|
||||
public SignaturePageViewModel(double width, double height)
|
||||
{
|
||||
if (width <= 0) throw new ArgumentOutOfRangeException(nameof(width));
|
||||
if (height <= 0) throw new ArgumentOutOfRangeException(nameof(height));
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bind a freshly-constructed (or re-templated) control to this
|
||||
/// VM. Called from the view's code-behind once the control has
|
||||
/// been added to the visual tree and its template applied (so
|
||||
/// <see cref="SignaturePadControl.CaptureArea"/> is wired).
|
||||
/// </summary>
|
||||
public void Attach(SignaturePadControl control)
|
||||
{
|
||||
if (control is null) throw new ArgumentNullException(nameof(control));
|
||||
Detach();
|
||||
_control = control;
|
||||
_control.RedrawRequested += OnRedraw;
|
||||
_control.StrokeCompleted += OnStrokeCompleted;
|
||||
RefreshCounts();
|
||||
}
|
||||
|
||||
public void Detach()
|
||||
{
|
||||
if (_control is null) return;
|
||||
_control.RedrawRequested -= OnRedraw;
|
||||
_control.StrokeCompleted -= OnStrokeCompleted;
|
||||
_control = null;
|
||||
}
|
||||
|
||||
private void OnStrokeCompleted(object? sender, SignaturePadData data)
|
||||
{
|
||||
StatusMessage = $"Trait terminé. {data.StrokeCount} trait(s).";
|
||||
RefreshCounts();
|
||||
}
|
||||
|
||||
private void OnRedraw(object? sender, EventArgs e) => RefreshCounts();
|
||||
|
||||
private void RefreshCounts()
|
||||
{
|
||||
if (_control is null) return;
|
||||
var snap = _control.Snapshot();
|
||||
StrokeCount = snap.StrokeCount;
|
||||
PointCount = snap.PointCount;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void Clear()
|
||||
{
|
||||
_control?.Clear();
|
||||
StatusMessage = "Effacé.";
|
||||
RefreshCounts();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task CaptureAsync()
|
||||
{
|
||||
if (_control is null)
|
||||
{
|
||||
StatusMessage = "Contrôle non attaché.";
|
||||
return;
|
||||
}
|
||||
|
||||
var data = _control.Snapshot();
|
||||
if (data.IsEmpty)
|
||||
{
|
||||
StatusMessage = "Rien à capturer.";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var path = WriteCapture(data);
|
||||
LastCapturedPath = path;
|
||||
StatusMessage = $"Capture enregistrée: {path}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Erreur: {ex.Message}";
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string WriteCapture(SignaturePadData data)
|
||||
{
|
||||
var dir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"PostIt", "signatures");
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
var fileName = $"signature-{DateTime.UtcNow:yyyyMMdd-HHmmssfff}.json";
|
||||
var path = Path.Combine(dir, fileName);
|
||||
|
||||
var payload = new
|
||||
{
|
||||
format = "yavsc.signature/v1",
|
||||
coordinateMax = SignaturePadData.CoordinateMax,
|
||||
capturedAtUtc = DateTime.UtcNow,
|
||||
strokes = data.Strokes,
|
||||
strokeCount = data.StrokeCount,
|
||||
};
|
||||
File.WriteAllText(
|
||||
path,
|
||||
JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true }),
|
||||
Encoding.UTF8);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,17 @@
|
|||
<Button Command="{Binding New}" Content="New post" />
|
||||
<Button Command="{Binding Save}" Content="Save" />
|
||||
<Button Command="{Binding Delete}" Content="Delete" />
|
||||
<!--
|
||||
DEV ONLY: temporary shortcut to open the signature
|
||||
capture page. Production entry point is a SignalR
|
||||
push from Yavsc.Org ("devis received, sign here").
|
||||
Remove this button and its Click handler in
|
||||
MainPage.axaml.cs once the SignalR handler lands.
|
||||
-->
|
||||
<Button x:Name="OpenSignatureDevButton"
|
||||
Content="[DEV] Signature"
|
||||
Click="OpenSignatureDev"
|
||||
ToolTip.Tip="DEV ONLY — to remove when SignalR handler lands" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using PostIt.ViewModels;
|
||||
|
||||
namespace PostIt.Views;
|
||||
|
||||
|
|
@ -10,4 +13,30 @@ public partial class MainPage : ContentPage
|
|||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DEV ONLY: temporary shortcut to open the signature capture
|
||||
/// page from the blog editor. The production entry point is a
|
||||
/// SignalR push from Yavsc.Org ("devis received, sign here"),
|
||||
/// which is the only path that carries the devis identifier
|
||||
/// needed to bind the capture to a specific contract.
|
||||
///
|
||||
/// Remove this method and the corresponding button in
|
||||
/// MainPage.axaml.cs once the SignalR handler lands.
|
||||
/// </summary>
|
||||
private void OpenSignatureDev(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
// Resolve via the App's DI container so the page gets
|
||||
// the canonical services (Api client, settings, ...).
|
||||
var app = Application.Current as App;
|
||||
var services = app?.Services;
|
||||
if (services is null) return;
|
||||
|
||||
var page = services.GetRequiredService<SignaturePage>();
|
||||
page.DataContext = services.GetRequiredService<SignaturePageViewModel>();
|
||||
|
||||
if (this.VisualRoot is MainWindow window)
|
||||
{
|
||||
_ = window.NavRoot.PushAsync(page);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
77
src/PostIt/PostIt/Views/SignaturePage.axaml
Normal file
77
src/PostIt/PostIt/Views/SignaturePage.axaml
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<ContentPage xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://github.com/avaloniaui/avalonia"
|
||||
xmlns:vm="using:PostIt.ViewModels"
|
||||
xmlns:controls="using:PostIt.Controls"
|
||||
x:Class="PostIt.Views.SignaturePage"
|
||||
x:DataType="vm:SignaturePageViewModel"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Design.DataContext>
|
||||
<vm:SignaturePageViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid Margin="12" RowSpacing="12"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="Capture de signature"
|
||||
FontSize="20"
|
||||
FontWeight="Bold" />
|
||||
|
||||
<!--
|
||||
Capture surface. The SignaturePadControl is render-
|
||||
agnostic, so we host it inside a fixed-size Border
|
||||
that is the PART_CaptureArea, and overlay a Canvas
|
||||
for the visual feedback. The view's code-behind
|
||||
repaints the canvas on every RedrawRequested.
|
||||
-->
|
||||
<Border Grid.Row="1"
|
||||
x:Name="PadFrame"
|
||||
Width="{Binding Width}"
|
||||
Height="{Binding Height}"
|
||||
BorderBrush="Black"
|
||||
BorderThickness="1"
|
||||
Background="White"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
ClipToBounds="True">
|
||||
<Grid>
|
||||
<controls:SignaturePadControl x:Name="Pad" />
|
||||
<Canvas x:Name="InkLayer"
|
||||
Background="Transparent"
|
||||
IsHitTestVisible="False" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Row="2"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Button Content="Effacer" Command="{Binding Clear}" />
|
||||
<Button Content="Capturer" Command="{Binding CaptureAsync}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Row="3"
|
||||
Text="{Binding StatusMessage}"
|
||||
Foreground="Gray"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock Grid.Row="4"
|
||||
FontSize="11"
|
||||
Foreground="DarkSlateGray"
|
||||
TextWrapping="Wrap">
|
||||
<Run Text="Strokes: " />
|
||||
<Run Text="{Binding StrokeCount, Mode=OneWay}" />
|
||||
<Run Text=" · Points: " />
|
||||
<Run Text="{Binding PointCount, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</ContentPage>
|
||||
91
src/PostIt/PostIt/Views/SignaturePage.axaml.cs
Normal file
91
src/PostIt/PostIt/Views/SignaturePage.axaml.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Media;
|
||||
using PostIt.Controls;
|
||||
using PostIt.ViewModels;
|
||||
|
||||
namespace PostIt.Views;
|
||||
|
||||
public partial class SignaturePage : ContentPage
|
||||
{
|
||||
private static readonly IBrush StrokeBrush = new SolidColorBrush(Color.FromRgb(0x10, 0x10, 0x10));
|
||||
private const double StrokeThickness = 2.0;
|
||||
private const double CoordinateMax = 10_000.0;
|
||||
|
||||
private SignaturePageViewModel? _vm;
|
||||
private SignaturePadControl? _control;
|
||||
|
||||
public SignaturePage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// Wire the capture area: the Pad itself is the control, the
|
||||
// surrounding Border (PadFrame) is the hit-test region. We
|
||||
// set CaptureArea once the control's template has been
|
||||
// applied — for an inline control with no template, that
|
||||
// happens on first measure, which is guaranteed before
|
||||
// the user can interact, so attaching here is safe.
|
||||
_control = Pad;
|
||||
_control.CaptureArea = PadFrame;
|
||||
|
||||
DataContextChanged += (_, _) => RebindViewModel(DataContext as SignaturePageViewModel);
|
||||
}
|
||||
|
||||
private void RebindViewModel(SignaturePageViewModel? vm)
|
||||
{
|
||||
if (_vm is not null)
|
||||
{
|
||||
_vm.Detach();
|
||||
_control!.RedrawRequested -= OnRedrawRequested;
|
||||
}
|
||||
|
||||
_vm = vm;
|
||||
|
||||
if (_vm is null || _control is null) return;
|
||||
|
||||
_vm.Attach(_control);
|
||||
_control.RedrawRequested += OnRedrawRequested;
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void OnRedrawRequested(object? sender, EventArgs e) => Repaint();
|
||||
|
||||
private void Repaint()
|
||||
{
|
||||
if (_control is null || InkLayer is null) return;
|
||||
|
||||
InkLayer.Children.Clear();
|
||||
var w = PadFrame.Bounds.Width;
|
||||
var h = PadFrame.Bounds.Height;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
var strokes = _control.Strokes;
|
||||
int i = 0;
|
||||
while (i < strokes.Count)
|
||||
{
|
||||
int k = strokes[i];
|
||||
if (k <= 0) break;
|
||||
i++; // skip the length prefix
|
||||
|
||||
var poly = new Polyline
|
||||
{
|
||||
Stroke = StrokeBrush,
|
||||
StrokeThickness = StrokeThickness,
|
||||
StrokeLineCap = PenLineCap.Round,
|
||||
StrokeJoin = PenLineJoin.Round,
|
||||
};
|
||||
var pts = new List<Point>(k);
|
||||
for (int p = 0; p < k; p++)
|
||||
{
|
||||
int nx = strokes[i++];
|
||||
int ny = strokes[i++];
|
||||
pts.Add(new Point(nx / CoordinateMax * w, ny / CoordinateMax * h));
|
||||
}
|
||||
poly.Points = pts;
|
||||
InkLayer.Children.Add(poly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,11 +14,11 @@ namespace Yavsc.Abstract.Chat
|
|||
|
||||
public const string JustCreatedBy = "just created by ";
|
||||
public const string LabYouNotOp = "you're no op.";
|
||||
public const string LabNoSuchUser = "No such user";
|
||||
public const string LabNoSuchChan = "No such chan";
|
||||
public const string LabNoSuchUser = "No such user";
|
||||
public const string LabNoSuchChan = "No such chan";
|
||||
public const string HopWontKickOp = "Half operator cannot kick any operator";
|
||||
public const string LabAuthChatUser = "Authenticated chat user";
|
||||
public const string NoKickOnCop = "No, you won´t, you´ĺl never do kick a cop, it is the bad.";
|
||||
public const string LabnoJoinNoSend = "LabnoJoinNoSend";
|
||||
public const string LabNoJoinNoSend = "LabnoJoinNoSend";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ namespace Yavsc.Services
|
|||
/// <summary>
|
||||
/// Renvoye la facture associée à une clé de facturation,
|
||||
/// à partir du couple suivant :
|
||||
///
|
||||
/// * un code de facturation
|
||||
///
|
||||
/// * un code de facturation
|
||||
/// (identifiant associé à un type de demande du client)
|
||||
/// * un entier long identifiant la demande du client
|
||||
/// (à une demande, on associe au maximum une seule facture)
|
||||
|
|
@ -26,10 +26,10 @@ namespace Yavsc.Services
|
|||
/// <param name="billingCode">Identifiant du type de facturation</param>
|
||||
/// <param name="queryId">Identifiant de la demande du client</param>
|
||||
/// <returns>La facture</returns>
|
||||
Task<IDecidableQuery> GetBillAsync(string billingCode, long queryId);
|
||||
|
||||
Task<IQuery> GetBillAsync(string billingCode, long queryId);
|
||||
|
||||
/// <summary>
|
||||
/// Perfomer settings for the specified performer in the activity
|
||||
/// Perfomer settings for the specified performer in the activity
|
||||
/// </summary>
|
||||
/// <param name="activityCode">activityCode</param>
|
||||
/// <param name="userId">performer uid</param>
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
// ------------------------------------------------------------------------------
|
||||
// <autogenerated>
|
||||
// This code was generated by a tool.
|
||||
// Mono Runtime Version: 4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </autogenerated>
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
namespace Yavsc {
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public partial class ChatHubLabels {
|
||||
|
||||
private static System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.Equals(null, resourceMan)) {
|
||||
System.Resources.ResourceManager temp = new System.Resources.ResourceManager(("Yavsc.Abstract.Resources." + "Yavsc.ChatHub"), typeof(ChatHubLabels).GetTypeInfo().Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
public static string Authenticated_chat_user {
|
||||
get {
|
||||
return ResourceManager.GetString("Authenticated chat user", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string LabnoJoinNoSend {
|
||||
get {
|
||||
return ResourceManager.GetString("LabnoJoinNoSend", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string InvalidRoomName {
|
||||
get {
|
||||
return ResourceManager.GetString("InvalidRoomName", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string InvalidUserName {
|
||||
get {
|
||||
return ResourceManager.GetString("InvalidUserName", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string InvalidMessage {
|
||||
get {
|
||||
return ResourceManager.GetString("InvalidMessage", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string InvalidReason {
|
||||
get {
|
||||
return ResourceManager.GetString("InvalidReason", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@ namespace Yavsc.Abstract.Workflow
|
|||
{
|
||||
public interface IDecidableQuery: ITrackedEntity, IQuery
|
||||
{
|
||||
bool Decided { get; set; }
|
||||
bool Accepted { get; set; }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,9 @@ namespace Yavsc
|
|||
public enum QueryStatus: int
|
||||
{
|
||||
Inserted,
|
||||
OwnerValidated,
|
||||
Visited,
|
||||
Rejected,
|
||||
Accepted,
|
||||
InProgress,
|
||||
|
||||
// final states
|
||||
Failed,
|
||||
Success
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ using Newtonsoft.Json;
|
|||
using System.Security.Claims;
|
||||
using Yavsc.Helpers;
|
||||
using Yavsc.ViewModels;
|
||||
using Yavsc.Models.Billing;
|
||||
using Yavsc.Server.Models.FileSystem;
|
||||
|
||||
namespace Yavsc.ApiControllers
|
||||
{
|
||||
|
|
@ -181,5 +183,203 @@ namespace Yavsc.ApiControllers
|
|||
if (!fi.Exists) return NotFound(new { Error = "Professional signature not found" });
|
||||
return File(fi.OpenRead(), "application/x-pdf", filename); ;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Capture a signature for an estimate, in the JSON
|
||||
/// wire format produced by PostIt (see
|
||||
/// <c>PostIt.Models.SignaturePadData</c>). The legacy
|
||||
/// <c>POST prosign</c> / <c>POST clisign</c> endpoints
|
||||
/// take a PNG <see cref="IFormFile"/>; this one takes a
|
||||
/// JSON body so the capture happens entirely in-app on
|
||||
/// the client side, without a rasterisation step.
|
||||
///
|
||||
/// <para>The route is intentionally a sibling of the
|
||||
/// legacy endpoints, not a replacement: the legacy
|
||||
/// PNG-based flow stays in place to keep the TeX
|
||||
/// invoice templates (<c>Bill_tex.cshtml</c>,
|
||||
/// <c>Estimate_tex.cshtml</c>) working until the
|
||||
/// migration commit regenerates PNGs from the JSON
|
||||
/// payload. The two flows share the
|
||||
/// <see cref="Signature"/> table for storage but not
|
||||
/// the URL surface.</para>
|
||||
/// </summary>
|
||||
[HttpPost("estimate/{id:long}/sign")]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Sign(
|
||||
[FromRoute] long id,
|
||||
[FromBody] SignatureSubmission body,
|
||||
CancellationToken token)
|
||||
{
|
||||
if (body is null) return BadRequest(new { Error = "missing body" });
|
||||
if (body.Strokes is null) return BadRequest(new { Error = "missing strokes" });
|
||||
if (string.IsNullOrEmpty(body.SignerUserId))
|
||||
return BadRequest(new { Error = "missing signerUserId" });
|
||||
|
||||
var estimate = await dbContext.Estimates
|
||||
.Include(e => e.Client)
|
||||
.FirstOrDefaultAsync(e => e.Id == id, token);
|
||||
if (estimate is null) return NotFound(new { Error = "estimate not found" });
|
||||
|
||||
// The signer is identified by userId in the body, not
|
||||
// by the bearer token, because the OAuth scope we
|
||||
// carry is for the API client (PostIt), not the end
|
||||
// user. We trust the body's userId to match either
|
||||
// Owner or Client, and reject everything else.
|
||||
var userId = body.SignerUserId;
|
||||
if (userId != estimate.OwnerId && userId != estimate.ClientId)
|
||||
return Forbid();
|
||||
|
||||
// Map userId → type. The Pro/Client split is the
|
||||
// same one the legacy prosign/clisign endpoints use;
|
||||
// keeping the rule here means the Signature table
|
||||
// and the legacy ProviderValidationDate/ClientValidationDate
|
||||
// columns can co-exist without contradicting each other.
|
||||
var type = userId == estimate.OwnerId
|
||||
? SignatureType.Pro
|
||||
: SignatureType.Client;
|
||||
|
||||
var payload = new SignaturePadPayload
|
||||
{
|
||||
CoordinateMax = body.CoordinateMax,
|
||||
CapturedAtUtc = body.CapturedAtUtc ?? DateTime.UtcNow,
|
||||
Strokes = body.Strokes,
|
||||
};
|
||||
|
||||
// Disk write first: a disk failure shouldn't leave
|
||||
// a Signature row pointing at a file that doesn't
|
||||
// exist. The file helper throws on filesystem
|
||||
// problems and propagates here.
|
||||
FileReceivedInfo fi;
|
||||
try
|
||||
{
|
||||
fi = await User.ReceiveEstimateSignatureAsync(id, type, payload, token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "estimate {Id}: signature file write failed", id);
|
||||
return BadRequest(new { Error = "file write failed", Detail = ex.Message });
|
||||
}
|
||||
|
||||
// Find-or-add: the (EstimateId, Type) pair is
|
||||
// unique, so a second POST for the same side of the
|
||||
// estimate replaces the previous signature. EF
|
||||
// translates this into a single UPDATE when the
|
||||
// row exists and an INSERT otherwise; the unique
|
||||
// index in ApplicationDbContext is the
|
||||
// database-level guarantee that the contract
|
||||
// holds if two requests race.
|
||||
var signature = await dbContext.Signatures
|
||||
.FirstOrDefaultAsync(s => s.EstimateId == id && s.Type == type, token);
|
||||
|
||||
if (signature is null)
|
||||
{
|
||||
signature = new Signature
|
||||
{
|
||||
EstimateId = id,
|
||||
SignerId = userId,
|
||||
Type = type,
|
||||
};
|
||||
dbContext.Signatures.Add(signature);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Roll the signer's quota back by the size of
|
||||
// the file we're about to orphan: the old
|
||||
// FilePath is no longer referenced once we
|
||||
// overwrite FilePath below.
|
||||
try
|
||||
{
|
||||
var orphan = new FileInfo(signature.FilePath);
|
||||
if (orphan.Exists)
|
||||
{
|
||||
var signerForOrphan = await dbContext.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == userId, token);
|
||||
if (signerForOrphan is not null)
|
||||
signerForOrphan.DiskUsage =
|
||||
Math.Max(0, signerForOrphan.DiskUsage - orphan.Length);
|
||||
}
|
||||
}
|
||||
catch { /* best effort — the file is being replaced anyway */ }
|
||||
}
|
||||
|
||||
signature.SignerId = userId;
|
||||
signature.CoordinateMax = payload.CoordinateMax;
|
||||
signature.Strokes = payload.Strokes;
|
||||
signature.CapturedAtUtc = payload.CapturedAtUtc;
|
||||
signature.FilePath = Path.Combine(fi.DestDir, fi.FileName);
|
||||
|
||||
// Bump the signer's quota. The Signature row's
|
||||
// SignerId is the IdentityUser.Id (a string), so we
|
||||
// look up by Id and not by username.
|
||||
var signer = await dbContext.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == userId, token);
|
||||
if (signer is not null)
|
||||
{
|
||||
signer.DiskUsage += new FileInfo(signature.FilePath).Length;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await dbContext.SaveChangesAsync(token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "estimate {Id}: signature db write failed", id);
|
||||
// Best-effort rollback: remove the file we wrote
|
||||
// so disk and db don't disagree.
|
||||
try { System.IO.File.Delete(signature.FilePath); }
|
||||
catch { /* swallow — the row will be re-orphaned, the user re-signs */ }
|
||||
return BadRequest(new { Error = "db write failed", Detail = ex.Message });
|
||||
}
|
||||
|
||||
var location = Url.Action(nameof(Sign), new { id })
|
||||
?? $"/api/bill/estimate/{id}/sign";
|
||||
return Created(location, new
|
||||
{
|
||||
id = signature.Id,
|
||||
estimateId = signature.EstimateId,
|
||||
type = signature.Type.ToString(),
|
||||
capturedAtUtc = signature.CapturedAtUtc,
|
||||
coordinateMax = signature.CoordinateMax,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON body of <c>POST /api/bill/estimate/{id}/sign</c>. The
|
||||
/// shape mirrors what PostIt sends; the <c>signerUserId</c>
|
||||
/// field disambiguates which side of the estimate signed
|
||||
/// because the bearer token belongs to the PostIt OAuth
|
||||
/// client, not the end user.
|
||||
/// </summary>
|
||||
public class SignatureSubmission
|
||||
{
|
||||
/// <summary>
|
||||
/// ApplicationUser.Id of the signer. Must equal
|
||||
/// <c>Estimate.OwnerId</c> for a Pro signature or
|
||||
/// <c>Estimate.ClientId</c> for a Client signature.
|
||||
/// </summary>
|
||||
public string SignerUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Wire-format strokes. See
|
||||
/// <c>PostIt.Models.SignaturePadData</c>.
|
||||
/// </summary>
|
||||
public int[] Strokes { get; set; } = Array.Empty<int>();
|
||||
|
||||
public int CoordinateMax { get; set; } = 10_000;
|
||||
|
||||
/// <summary>
|
||||
/// Client-reported capture time. The server may override
|
||||
/// this with <c>DateTime.UtcNow</c> if the client is
|
||||
/// caught lying about clock skew, but the default is to
|
||||
/// trust the client.
|
||||
/// </summary>
|
||||
public DateTime? CapturedAtUtc { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ namespace Yavsc.ApiControllers
|
|||
if (queryId == 0) return BadRequest("queryId");
|
||||
var billing = BillingService.GetBillable(dbContext, billingCode, queryId);
|
||||
if (billing == null) return BadRequest();
|
||||
billing.Decided = true;
|
||||
billing.Accepted = false;
|
||||
|
||||
billing.Status = QueryStatus.Rejected;
|
||||
dbContext.SaveChanges();
|
||||
return Ok();
|
||||
}
|
||||
|
|
@ -47,8 +47,7 @@ namespace Yavsc.ApiControllers
|
|||
if (queryId == 0) return BadRequest("queryId");
|
||||
var billing = BillingService.GetBillable(dbContext, billingCode, queryId);
|
||||
if (billing == null) return BadRequest();
|
||||
billing.Accepted = true;
|
||||
billing.Decided = true;
|
||||
billing.Status = QueryStatus.Accepted;
|
||||
dbContext.SaveChanges();
|
||||
return Ok();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Yavsc.Attributes.Validation;
|
||||
using Yavsc.Helpers;
|
||||
using Yavsc.Models;
|
||||
using Yavsc.Models.Messaging;
|
||||
using Yavsc.Services;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Yavsc.Server.Helpers;
|
||||
using static Yavsc.Blogs.Constants;
|
||||
using Yavsc.Server.Hubs;
|
||||
|
||||
namespace Yavsc.Blogs.Controllers
|
||||
{
|
||||
|
|
@ -46,7 +46,7 @@ namespace Yavsc.Blogs.Controllers
|
|||
}
|
||||
logger.LogInformation("validated: api/stream/Put: "+filename);
|
||||
var userName = User.GetUserName();
|
||||
|
||||
|
||||
string url = string.Format(
|
||||
"{0}/{1}/{2}",
|
||||
Config.UserFilesOptions.RequestPath.ToUriComponent(),
|
||||
|
|
@ -54,7 +54,7 @@ namespace Yavsc.Blogs.Controllers
|
|||
filename
|
||||
);
|
||||
|
||||
|
||||
|
||||
string destDir = HttpContext.User.EnsureDestinationDirectory(filePath);
|
||||
logger.LogInformation($"Saving flow to {destDir}");
|
||||
var userId = User.GetUserId();
|
||||
|
|
@ -65,7 +65,7 @@ namespace Yavsc.Blogs.Controllers
|
|||
sender = userName,
|
||||
url = url,
|
||||
}, $"{userName} is starting a stream!");
|
||||
|
||||
|
||||
await liveProcessor.AcceptStream(HttpContext, user, destDir, shortFileName);
|
||||
return Ok();
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Yavsc.Org.Tests.Controllers;
|
||||
|
||||
public class CommandFormsControllerTests : IClassFixture<TestWebApplicationFactory>
|
||||
{
|
||||
private readonly TestWebApplicationFactory _factory;
|
||||
|
||||
public CommandFormsControllerTests(TestWebApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
private HttpClient CreateAdminClient()
|
||||
{
|
||||
var http = _factory.CreateClient(new WebApplicationFactoryClientOptions
|
||||
{
|
||||
HandleCookies = true,
|
||||
});
|
||||
http.DefaultRequestHeaders.Add(TestAuthPolicyProvider.HeaderName, TestAuthPolicyProvider.AdminRole);
|
||||
return http;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_GET_returns_200_for_admin()
|
||||
{
|
||||
var http = CreateAdminClient();
|
||||
var response = await http.GetAsync("/CommandForms/Create", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
||||
Assert.Contains("Create", body);
|
||||
}
|
||||
}
|
||||
134
src/Yavsc.Org.Tests/EstimateSignatureFileHelperTests.cs
Normal file
134
src/Yavsc.Org.Tests/EstimateSignatureFileHelperTests.cs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using Yavsc.Models;
|
||||
using Yavsc.Models.Billing;
|
||||
using Yavsc.Server.Helpers;
|
||||
using Yavsc.Server.Models.FileSystem;
|
||||
|
||||
namespace Yavsc.Org.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="EstimateSignatureFileHelper"/> static
|
||||
/// helper. Scope is intentionally narrow: the file-naming format,
|
||||
/// the strokes counter, and the on-disk write path. The controller
|
||||
/// (authz, db persistence, signalR notification) is out of scope
|
||||
/// for this commit and will get a dedicated integration test once
|
||||
/// the Yavsc.Api test project is set up.
|
||||
/// </summary>
|
||||
public class EstimateSignatureFileHelperTests : IDisposable
|
||||
{
|
||||
private readonly string _tempRoot;
|
||||
|
||||
public EstimateSignatureFileHelperTests()
|
||||
{
|
||||
// UserFilesDirName is a process-wide static; we redirect
|
||||
// it to a per-test temp dir so concurrent tests don't
|
||||
// collide and the host filesystem is not littered.
|
||||
_tempRoot = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"yavsc-sig-tests-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(_tempRoot);
|
||||
AbstractFileSystemHelpers.UserFilesDirName = _tempRoot;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_tempRoot, recursive: true); }
|
||||
catch { /* best effort — the OS will clean Temp eventually */ }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FileNameFormat_lowercases_type_and_includes_estimateId_and_ticks()
|
||||
{
|
||||
var name = EstimateSignatureFileHelper.FileNameFormat(
|
||||
SignatureType.Pro, 42, 638_000_000_000_000_000L);
|
||||
Assert.Equal("sign-pro-42-638000000000000000.json", name);
|
||||
|
||||
var cli = EstimateSignatureFileHelper.FileNameFormat(
|
||||
SignatureType.Client, 7, 1L);
|
||||
Assert.Equal("sign-client-7-1.json", cli);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new int[] { }, 0)]
|
||||
[InlineData(new[] { 1, 100, 200 }, 1)]
|
||||
[InlineData(new[] { 2, 1, 2, 3, 4 }, 1)]
|
||||
[InlineData(new[] { 1, 1, 1, 2, 2, 3, 3 }, 2)]
|
||||
[InlineData(new[] { 0, 1, 2, 3 }, 0)] // malformed k=0: short-circuit
|
||||
public void ReceiveEstimateSignatureAsync_writes_a_v1_envelope(int[] strokes, int expectedStrokeCount)
|
||||
{
|
||||
// We don't read the count back from the helper (it's a
|
||||
// private method), but the JSON envelope must reflect
|
||||
// it; this verifies the public behaviour end-to-end.
|
||||
_ = expectedStrokeCount;
|
||||
// Arrange
|
||||
var user = MakeUser("alice");
|
||||
var payload = new SignaturePadPayload
|
||||
{
|
||||
CoordinateMax = 10_000,
|
||||
CapturedAtUtc = new DateTime(2026, 7, 4, 12, 0, 0, DateTimeKind.Utc),
|
||||
Strokes = strokes,
|
||||
};
|
||||
|
||||
// Act
|
||||
var fi = Run(user, 123L, SignatureType.Pro, payload);
|
||||
|
||||
// Assert: file exists, sits under the user's root, and
|
||||
// parses as a yavsc.signature/v1 envelope.
|
||||
var fullPath = Path.Combine(fi.DestDir, fi.FileName);
|
||||
Assert.True(File.Exists(fullPath), $"missing: {fullPath}");
|
||||
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(fullPath));
|
||||
var root = doc.RootElement;
|
||||
Assert.Equal("yavsc.signature/v1", root.GetProperty("format").GetString());
|
||||
Assert.Equal(10_000, root.GetProperty("coordinateMax").GetInt32());
|
||||
Assert.Equal(123L, root.GetProperty("estimateId").GetInt64());
|
||||
Assert.Equal("Pro", root.GetProperty("type").GetString());
|
||||
Assert.Equal("alice", root.GetProperty("signerName").GetString());
|
||||
Assert.Equal(expectedStrokeCount, root.GetProperty("strokeCount").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReceiveEstimateSignatureAsync_rejects_null_payload()
|
||||
{
|
||||
var user = MakeUser("bob");
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() =>
|
||||
EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync(
|
||||
user, 1L, SignatureType.Pro, payload: null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReceiveEstimateSignatureAsync_rejects_non_positive_estimateId()
|
||||
{
|
||||
var user = MakeUser("bob");
|
||||
var payload = new SignaturePadPayload { Strokes = new[] { 1, 100, 100 } };
|
||||
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
|
||||
EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync(
|
||||
user, 0L, SignatureType.Pro, payload));
|
||||
}
|
||||
|
||||
// --- helpers ----------------------------------------------------
|
||||
|
||||
private static FileReceivedInfo Run(
|
||||
ClaimsPrincipal user, long estimateId, SignatureType type, SignaturePadPayload payload)
|
||||
{
|
||||
// The helper is async; tests that don't care about the
|
||||
// result can call it sync via .GetAwaiter().GetResult()
|
||||
// because we know it never throws in the happy path.
|
||||
return EstimateSignatureFileHelper
|
||||
.ReceiveEstimateSignatureAsync(user, estimateId, type, payload, CancellationToken.None)
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static ClaimsPrincipal MakeUser(string username)
|
||||
{
|
||||
return new ClaimsPrincipal(new ClaimsIdentity(
|
||||
new[] { new Claim(ClaimTypes.Name, username) },
|
||||
authenticationType: "test"));
|
||||
}
|
||||
}
|
||||
|
|
@ -36,11 +36,11 @@ namespace Yavsc
|
|||
{
|
||||
WorkflowHelpers.ConfigureBillingService();
|
||||
|
||||
var firstRegistrar = new Func<ApplicationDbContext, long, IDecidableQuery>((db, id) =>
|
||||
db.HairCutQueries.Include(q => q.Prestation).Include(q => q.Regularisation).Single(q => q.Id == id));
|
||||
var firstRegistrar = new Func<ApplicationDbContext, long, IQuery>((db, id) =>
|
||||
db.HairCutQueries.Include(q => q.Prestation).Include(q => q.Regularization).Single(q => q.Id == id));
|
||||
|
||||
const string testCode = "Brush";
|
||||
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
WorkflowHelpers.RegisterBilling<HairCutQuery>(testCode, firstRegistrar));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,10 +93,10 @@ namespace Yavsc.Controllers
|
|||
: "";
|
||||
|
||||
var user = await GetCurrentUserAsync();
|
||||
|
||||
|
||||
long pc = _dbContext.BlogSpot.Count(x => x.AuthorId == user.Id);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var model = new IndexViewModel
|
||||
{
|
||||
|
|
@ -123,14 +123,14 @@ namespace Yavsc.Controllers
|
|||
AllowMonthlyEmail = user.AllowMonthlyEmail,
|
||||
Address = user.PostalAddress?.Address
|
||||
};
|
||||
|
||||
|
||||
model.HaveProfessionalSettings = _dbContext.Performers.Any(x => x.PerformerId == user.Id);
|
||||
var usrActs = _dbContext.UserActivities.Include(a=>a.Does).Where(a=> a.UserId == user.Id).ToArray();
|
||||
// TODO remember me who this magical a.Settings is built
|
||||
var usrActToSet = usrActs.Where( a => ( a.Settings == null && a.Does.SettingsClassName != null )).ToArray();
|
||||
model.HaveActivityToConfigure = usrActToSet .Count()>0;
|
||||
model.Activity = _dbContext.UserActivities.Include(a=>a.Does).Where(u=>u.UserId == user.Id).ToList();
|
||||
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ namespace Yavsc.Controllers
|
|||
var user = await GetCurrentUserAsync();
|
||||
user.AllowMonthlyEmail = model.Allow;
|
||||
await this._dbContext.SaveChangesAsync(User.GetUserId());
|
||||
|
||||
|
||||
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.SetMonthlyEmailSuccess });
|
||||
}
|
||||
|
||||
|
|
@ -302,8 +302,8 @@ namespace Yavsc.Controllers
|
|||
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
|
||||
var calendars = await _calendarManager.GetCalendarsAsync(pageToken);
|
||||
return View(new SetGoogleCalendarViewModel {
|
||||
ReturnUrl = returnUrl,
|
||||
return View(new SetGoogleCalendarViewModel {
|
||||
ReturnUrl = returnUrl,
|
||||
Calendars = calendars
|
||||
});
|
||||
}
|
||||
|
|
@ -343,9 +343,9 @@ namespace Yavsc.Controllers
|
|||
)) return BadRequest(new { message = "data already present" });
|
||||
|
||||
user.BankInfo.Add(model);
|
||||
|
||||
|
||||
_dbContext.Update(user);
|
||||
|
||||
|
||||
await _dbContext.SaveChangesAsync();
|
||||
}
|
||||
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.SetBankInfoSuccess });
|
||||
|
|
@ -495,7 +495,7 @@ namespace Yavsc.Controllers
|
|||
return View("Error");
|
||||
}
|
||||
var userLogins = await _userManager.GetLoginsAsync(user);
|
||||
|
||||
|
||||
ViewBag.ShowRemoveButton = user.PasswordHash != null || userLogins.Count > 1;
|
||||
|
||||
return View(new ManageLoginsViewModel
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Yavsc.Helpers;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Yavsc.Models;
|
||||
using Yavsc.Models.Workflow;
|
||||
using Yavsc.Server.Helpers;
|
||||
using Yavsc.Services;
|
||||
|
||||
namespace Yavsc.Controllers
|
||||
{
|
||||
public class CommandFormsController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly IStringLocalizer<CommandFormsController> _localizer;
|
||||
|
||||
public CommandFormsController(ApplicationDbContext context)
|
||||
public CommandFormsController(ApplicationDbContext context,
|
||||
IStringLocalizer<CommandFormsController> localizer)
|
||||
{
|
||||
_context = context;
|
||||
_localizer = localizer;
|
||||
}
|
||||
|
||||
// GET: CommandForms
|
||||
|
|
@ -47,11 +51,14 @@ namespace Yavsc.Controllers
|
|||
SetViewBag();
|
||||
return View();
|
||||
}
|
||||
|
||||
private void SetViewBag(CommandForm commandForm = null)
|
||||
{
|
||||
ViewBag.ActivityCode = new SelectList(_context.Activities, "Code", "Name", commandForm?.ActivityCode);
|
||||
ViewBag.ActionName = _context.CommandForm.Select(c => new SelectListItem { Value = c.Id.ToString(), Text = c.Title, Selected = commandForm.Id == c.Id });
|
||||
ViewBag.ActionName = BillingService.Billing.Keys
|
||||
.Select((string b) => new SelectListItem { Value = b, Text = _localizer[b] }).ToList();
|
||||
}
|
||||
|
||||
// POST: CommandForms/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
|
|
|
|||
|
|
@ -36,16 +36,16 @@ namespace Yavsc.Controllers
|
|||
public ActionResult Index()
|
||||
{
|
||||
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var now = DateTime.Now;
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var model = new FrontOfficeIndexViewModel
|
||||
{
|
||||
EstimateToProduceCount = _context.RdvQueries.Where(c => c.PerformerId == uid && c.EventDate > now
|
||||
&& c.ValidationDate == null && !_context.Estimates.Any(e => (e.CommandId == c.Id && e.ProviderValidationDate != null))).Count(),
|
||||
EstimateToSignAsProCount = _context.RdvQueries.Where(c => (c.PerformerId == uid && c.EventDate > now
|
||||
&& c.ValidationDate == null && _context.Estimates.Any(e => (e.CommandId == c.Id && e.ProviderValidationDate != null)))).Count(),
|
||||
EstimateToSignAsCliCount = _context.Estimates.Where(e => e.ClientId == uid && e.ClientValidationDate == null).Count(),
|
||||
BillToSignAsProCount = 0,
|
||||
EstimateToProduceCount = _context.RdvQueries.Where(c => c.PerformerId == uid && c.EventDate > now && c.Status == QueryStatus.Inserted
|
||||
&& c.ValidationDate == null && !_context.Estimates.Any(e => e.CommandId == c.Id)).Count(),
|
||||
EstimateToHonorAsProCount = _context.RdvQueries.Where(c => c.PerformerId == uid && c.EventDate > now && c.Status == QueryStatus.Accepted
|
||||
&& c.ValidationDate == null && _context.Estimates.Any(e => e.CommandId == c.Id )).Count(),
|
||||
EstimateToSignAsCliCount = _context.Estimates.Where(e => e.ClientId == uid && e.Query.Status == QueryStatus.Accepted).Count(),
|
||||
|
||||
BillToSignAsCliCount = 0,
|
||||
NewPayementsCount = 0
|
||||
};
|
||||
|
|
@ -65,14 +65,14 @@ namespace Yavsc.Controllers
|
|||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
public async Task <ActionResult> HairCut(string id)
|
||||
public async Task <ActionResult> ListPerformersAsync(string activityCode)
|
||||
{
|
||||
if (id == null)
|
||||
if (activityCode == null)
|
||||
{
|
||||
throw new NotImplementedException("No Activity code");
|
||||
}
|
||||
ViewBag.Activity = await _context.Activities.FirstOrDefaultAsync(a => a.Code == id);
|
||||
var result = await _context.ListPerformersAsync(_billing, id);
|
||||
ViewBag.Activity = await _context.Activities.FirstOrDefaultAsync(a => a.Code == activityCode);
|
||||
var result = await _context.ListPerformersAsync(_billing, activityCode);
|
||||
return View(result);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ namespace Yavsc.Controllers.Generic
|
|||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Models;
|
||||
using Yavsc.Helpers;
|
||||
using Yavsc.Server.Helpers;
|
||||
using Yavsc.Services;
|
||||
|
||||
|
|
@ -37,7 +36,7 @@ namespace Yavsc.Controllers.Generic
|
|||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
return View(await GetSettingsAsync(User.GetUserId()));
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
|
@ -23,7 +20,7 @@ namespace Yavsc.Extensions
|
|||
var typeInfo = type.GetTypeInfo();
|
||||
var values = Enum.GetValues(type).Cast<Enum>();
|
||||
var items = new List<SelectListItem>();
|
||||
|
||||
|
||||
foreach (var value in values)
|
||||
{
|
||||
items.Add(new SelectListItem {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ using Yavsc.Settings;
|
|||
using Yavsc.ViewModels.Auth;
|
||||
using IdentityServer8.Models;
|
||||
using IdentityServer8.EntityFramework.Mappers;
|
||||
using Yavsc.Server.Hubs;
|
||||
|
||||
namespace Yavsc.Extensions;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -12,12 +12,16 @@ namespace Yavsc.Helpers {
|
|||
this ApplicationDbContext _dbContext, List<UserActivity> activity)
|
||||
{
|
||||
var activities = activity.ToArray();
|
||||
var activityCodes = activities.Select(a=>a.DoesCode).ToArray();
|
||||
|
||||
List<SelectListItem> items = _dbContext.Activities.Select(
|
||||
var systemActivities = _dbContext.Activities.Where(a=>!a.Moderated
|
||||
&& activityCodes.Contains(a.Code)).ToArray();
|
||||
|
||||
List<SelectListItem> items = systemActivities.Select(
|
||||
x=> new SelectListItem() {
|
||||
Value = x.Code, Text = x.Name, Selected = activities.Any(a=>a.DoesCode == x.Code)
|
||||
} ).ToList();
|
||||
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4696
src/Yavsc.Org/Migrations/20260704154837_moderatedActivities.Designer.cs
generated
Normal file
4696
src/Yavsc.Org/Migrations/20260704154837_moderatedActivities.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
138
src/Yavsc.Org/Migrations/20260704154837_moderatedActivities.cs
Normal file
138
src/Yavsc.Org/Migrations/20260704154837_moderatedActivities.cs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Yavsc.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class moderatedActivities : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_MusicalPreference_GeneralSettings_GeneralSettingsUserId",
|
||||
table: "MusicalPreference");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "GeneralSettingsUserId",
|
||||
table: "MusicalPreference",
|
||||
newName: "MusicLoverSettingsUserId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_MusicalPreference_GeneralSettingsUserId",
|
||||
table: "MusicalPreference",
|
||||
newName: "IX_MusicalPreference_MusicLoverSettingsUserId");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Description",
|
||||
table: "Bug",
|
||||
type: "character varying(10240)",
|
||||
maxLength: 10240,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Moderated",
|
||||
table: "Activities",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Signatures",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
EstimateId = table.Column<long>(type: "bigint", nullable: false),
|
||||
SignerId = table.Column<string>(type: "text", nullable: false),
|
||||
Type = table.Column<int>(type: "integer", nullable: false),
|
||||
CoordinateMax = table.Column<int>(type: "integer", nullable: false, defaultValue: 10000),
|
||||
Strokes = table.Column<int[]>(type: "integer[]", nullable: false),
|
||||
CapturedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
FilePath = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Signatures", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Signatures_AspNetUsers_SignerId",
|
||||
column: x => x.SignerId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Signatures_Estimates_EstimateId",
|
||||
column: x => x.EstimateId,
|
||||
principalTable: "Estimates",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Signatures_EstimateId_Type",
|
||||
table: "Signatures",
|
||||
columns: new[] { "EstimateId", "Type" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Signatures_SignerId",
|
||||
table: "Signatures",
|
||||
column: "SignerId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_MusicalPreference_GeneralSettings_MusicLoverSettingsUserId",
|
||||
table: "MusicalPreference",
|
||||
column: "MusicLoverSettingsUserId",
|
||||
principalTable: "GeneralSettings",
|
||||
principalColumn: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_MusicalPreference_GeneralSettings_MusicLoverSettingsUserId",
|
||||
table: "MusicalPreference");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Signatures");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Moderated",
|
||||
table: "Activities");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "MusicLoverSettingsUserId",
|
||||
table: "MusicalPreference",
|
||||
newName: "GeneralSettingsUserId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_MusicalPreference_MusicLoverSettingsUserId",
|
||||
table: "MusicalPreference",
|
||||
newName: "IX_MusicalPreference_GeneralSettingsUserId");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Description",
|
||||
table: "Bug",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(10240)",
|
||||
oldMaxLength: 10240,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_MusicalPreference_GeneralSettings_GeneralSettingsUserId",
|
||||
table: "MusicalPreference",
|
||||
column: "GeneralSettingsUserId",
|
||||
principalTable: "GeneralSettings",
|
||||
principalColumn: "UserId");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ namespace Yavsc.Migrations
|
|||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
|
@ -1413,6 +1413,50 @@ namespace Yavsc.Migrations
|
|||
b.ToTable("ExceptionsSIREN");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Yavsc.Models.Billing.Signature", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("CapturedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("CoordinateMax")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasDefaultValue(10000);
|
||||
|
||||
b.Property<long>("EstimateId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("FilePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SignerId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.PrimitiveCollection<int[]>("Strokes")
|
||||
.IsRequired()
|
||||
.HasColumnType("integer[]");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SignerId");
|
||||
|
||||
b.HasIndex("EstimateId", "Type")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Signatures");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b =>
|
||||
{
|
||||
b.Property<long>("FileId")
|
||||
|
|
@ -2157,7 +2201,8 @@ namespace Yavsc.Migrations
|
|||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(10240)
|
||||
.HasColumnType("character varying(10240)");
|
||||
|
||||
b.Property<long?>("FeatureId")
|
||||
.HasColumnType("bigint");
|
||||
|
|
@ -2534,7 +2579,7 @@ namespace Yavsc.Migrations
|
|||
b.Property<string>("DjSettingsUserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("GeneralSettingsUserId")
|
||||
b.Property<string>("MusicLoverSettingsUserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Rate")
|
||||
|
|
@ -2547,7 +2592,7 @@ namespace Yavsc.Migrations
|
|||
|
||||
b.HasIndex("DjSettingsUserId");
|
||||
|
||||
b.HasIndex("GeneralSettingsUserId");
|
||||
b.HasIndex("MusicLoverSettingsUserId");
|
||||
|
||||
b.HasIndex("TendencyId");
|
||||
|
||||
|
|
@ -2585,16 +2630,6 @@ namespace Yavsc.Migrations
|
|||
b.ToTable("DjSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Yavsc.Models.Musical.Profiles.GeneralSettings", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId");
|
||||
|
||||
b.ToTable("GeneralSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b =>
|
||||
{
|
||||
b.Property<long>("InstrumentId")
|
||||
|
|
@ -2610,6 +2645,16 @@ namespace Yavsc.Migrations
|
|||
b.ToTable("Instrumentation");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId");
|
||||
|
||||
b.ToTable("GeneralSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b =>
|
||||
{
|
||||
b.Property<string>("CreationToken")
|
||||
|
|
@ -2894,6 +2939,9 @@ namespace Yavsc.Migrations
|
|||
b.Property<bool>("Hidden")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("Moderated")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("ModeratorGroupName")
|
||||
.HasColumnType("text");
|
||||
|
||||
|
|
@ -3737,6 +3785,25 @@ namespace Yavsc.Migrations
|
|||
b.Navigation("Query");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Yavsc.Models.Billing.Signature", b =>
|
||||
{
|
||||
b.HasOne("Yavsc.Models.Billing.Estimate", "Estimate")
|
||||
.WithMany("Signatures")
|
||||
.HasForeignKey("EstimateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Yavsc.Models.ApplicationUser", "Signer")
|
||||
.WithMany()
|
||||
.HasForeignKey("SignerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Estimate");
|
||||
|
||||
b.Navigation("Signer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b =>
|
||||
{
|
||||
b.HasOne("Yavsc.Models.Blog.UploadedFile", "File")
|
||||
|
|
@ -4162,9 +4229,9 @@ namespace Yavsc.Migrations
|
|||
.WithMany("SoundColor")
|
||||
.HasForeignKey("DjSettingsUserId");
|
||||
|
||||
b.HasOne("Yavsc.Models.Musical.Profiles.GeneralSettings", null)
|
||||
b.HasOne("Yavsc.Models.Musical.Profiles.MusicLoverSettings", null)
|
||||
.WithMany("SoundColor")
|
||||
.HasForeignKey("GeneralSettingsUserId");
|
||||
.HasForeignKey("MusicLoverSettingsUserId");
|
||||
|
||||
b.HasOne("Yavsc.Models.Musical.MusicalTendency", "MusicalTendency")
|
||||
.WithMany()
|
||||
|
|
@ -4519,6 +4586,8 @@ namespace Yavsc.Migrations
|
|||
modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b =>
|
||||
{
|
||||
b.Navigation("Bill");
|
||||
|
||||
b.Navigation("Signatures");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b =>
|
||||
|
|
@ -4580,7 +4649,7 @@ namespace Yavsc.Migrations
|
|||
b.Navigation("SoundColor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Yavsc.Models.Musical.Profiles.GeneralSettings", b =>
|
||||
modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b =>
|
||||
{
|
||||
b.Navigation("SoundColor");
|
||||
});
|
||||
|
|
|
|||
65
src/Yavsc.Org/Resources/Views/CommandForms/Index.resx
Normal file
65
src/Yavsc.Org/Resources/Views/CommandForms/Index.resx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<!--
|
||||
route name for the api controller used to tag the 'BlogPost' entity
|
||||
-->
|
||||
<data name="CommandForms"><value>Formulaires de commandes</value></data>
|
||||
</root>
|
||||
65
src/Yavsc.Org/Resources/Views/Manage/SetActivity.resx
Normal file
65
src/Yavsc.Org/Resources/Views/Manage/SetActivity.resx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<!--
|
||||
route name for the api controller used to tag the 'BlogPost' entity
|
||||
-->
|
||||
<data name="AcceptPublicContact"><value>Accepte les demandes de prise de contact d'utilisateurs authentifié, sans plus de procès</value></data>
|
||||
</root>
|
||||
|
|
@ -80,7 +80,7 @@ namespace Yavsc.Services
|
|||
public void OnConnected(string cxId, bool isCop)
|
||||
{
|
||||
var username = ChatUserNames[cxId];
|
||||
if (!IsConnected(username))
|
||||
if (!IsConnected(username))
|
||||
ChatRoomPresence[username] = new List<string>();
|
||||
_isCop[username] = isCop;
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ namespace Yavsc.Services
|
|||
return _isCop[userName];
|
||||
}
|
||||
|
||||
public void OnDisctonnected(string connectionId)
|
||||
public void OnDisconnected(string connectionId)
|
||||
{
|
||||
string uname;
|
||||
|
||||
|
|
@ -136,7 +136,7 @@ namespace Yavsc.Services
|
|||
return false;
|
||||
}
|
||||
// FIXME only remove cx, not username,
|
||||
// as long as he might be connected
|
||||
// as long as he might be connected
|
||||
// from another device, to the same room
|
||||
chanInfo.Users.Remove(cxId);
|
||||
if (chanInfo.Users.Count == 0)
|
||||
|
|
@ -183,7 +183,7 @@ namespace Yavsc.Services
|
|||
}
|
||||
else{
|
||||
chanInfo.Users.Add(cxId);
|
||||
}
|
||||
}
|
||||
_logger.LogInformation($"existing room joint: {userName}=>{roomName}");
|
||||
if (!ChatRoomPresence[userName].Contains(roomName))
|
||||
ChatRoomPresence[userName].Add(roomName);
|
||||
|
|
@ -200,7 +200,7 @@ namespace Yavsc.Services
|
|||
// room was closed.
|
||||
var room = _dbContext.ChatRoom.FirstOrDefault(r => r.Name == roomName);
|
||||
chanInfo = new ChatRoomInfo();
|
||||
|
||||
|
||||
|
||||
if (room != null)
|
||||
{
|
||||
|
|
@ -244,7 +244,7 @@ namespace Yavsc.Services
|
|||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public bool Dehop(string roomName, string userName)
|
||||
public bool DeHop(string roomName, string userName)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
|
@ -292,12 +292,12 @@ namespace Yavsc.Services
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!Channels.TryGetValue(roomName, out chanInfo))
|
||||
if (!Channels.TryGetValue(roomName, out chanInfo))
|
||||
{
|
||||
_errorHandler(roomName, _localizer.GetString(ChatHubConstants.LabNoSuchChan).ToString());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
var kickerName = GetUserName(cxId);
|
||||
if (!chanInfo.Ops.Contains(cxId))
|
||||
if (!chanInfo.Hops.Contains(cxId))
|
||||
|
|
@ -325,19 +325,19 @@ namespace Yavsc.Services
|
|||
}
|
||||
|
||||
// all good, time to kick :-)
|
||||
foreach (var ucx in ucxs) {
|
||||
if (chanInfo.Users.Contains(ucx))
|
||||
foreach (var ucx in ucxs) {
|
||||
if (chanInfo.Users.Contains(ucx))
|
||||
chanInfo.Users.Remove(ucx);
|
||||
|
||||
else if (chanInfo.Ops.Contains(ucx))
|
||||
else if (chanInfo.Ops.Contains(ucx))
|
||||
chanInfo.Ops.Remove(ucx);
|
||||
|
||||
else if (chanInfo.Hops.Contains(ucx))
|
||||
else if (chanInfo.Hops.Contains(ucx))
|
||||
chanInfo.Hops.Remove(ucx);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using Yavsc.Interface;
|
||||
|
|
@ -8,6 +7,7 @@ using Yavsc.Models;
|
|||
using Yavsc.Models.Google.Messaging;
|
||||
using Yavsc.Models.Haircut;
|
||||
using Yavsc.Models.Messaging;
|
||||
using Yavsc.Server.Hubs;
|
||||
|
||||
namespace Yavsc.Services
|
||||
{
|
||||
|
|
@ -92,25 +92,25 @@ namespace Yavsc.Services
|
|||
|
||||
|
||||
_logger.LogDebug($"Sending to {user.UserName} <{user.Email}> : {body}");
|
||||
|
||||
|
||||
result.message_id = await _emailSender.SendEmailAsync(user.UserName, user.Email,
|
||||
$"{ev.Sender} (un client) vous demande un rendez-vous",
|
||||
body + Environment.NewLine);
|
||||
response.success++;
|
||||
|
||||
var cxids = _cxManager.GetConnexionIds(user.UserName);
|
||||
if (cxids == null)
|
||||
|
||||
var cxIds = _cxManager.GetConnexionIds(user.UserName);
|
||||
if (cxIds == null)
|
||||
{
|
||||
_logger.LogDebug($"no cx to {user.UserName} <{user.Email}> ");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug($"Sending signal to {string.Join(" ", cxids)} : " + JsonConvert.SerializeObject(ev));
|
||||
_logger.LogDebug($"Sending signal to {string.Join(" ", cxIds)} : " + JsonConvert.SerializeObject(ev));
|
||||
|
||||
foreach (var cxid in cxids)
|
||||
foreach (var cxId in cxIds)
|
||||
{
|
||||
// from usr asp.net Id : var hubClient = hubContext.Clients.User(userId);
|
||||
var hubClient = hubContext.Clients.Client(cxid);
|
||||
var hubClient = hubContext.Clients.Client(cxId);
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
["event"] = JsonConvert.SerializeObject(ev)
|
||||
|
|
@ -152,22 +152,9 @@ namespace Yavsc.Services
|
|||
return await NotifyEvent<HairCutQueryEvent>(userIds, ev);
|
||||
}
|
||||
|
||||
public async Task<MessageWithPayloadResponse> NotifyAsync(IEnumerable<string> userIds, IEvent yaev)
|
||||
public async Task<MessageWithPayloadResponse> NotifyAsync(IEnumerable<string> userIds, IEvent yavscEvent)
|
||||
{
|
||||
return await NotifyEvent<IEvent>(userIds, yaev);
|
||||
return await NotifyEvent<IEvent>(userIds, yavscEvent);
|
||||
}
|
||||
|
||||
/* SMS with Twilio:
|
||||
public Task SendSmsAsync(TwilioSettings twilioSettigns, string number, string message)
|
||||
{
|
||||
var Twilio = new TwilioRestClient(twilioSettigns.AccountSID, twilioSettigns.Token);
|
||||
var result = Twilio.SendMessage( twilioSettigns.SMSAccountFrom, number, message);
|
||||
// Status is one of Queued, Sending, Sent, Failed or null if the number is not valid
|
||||
Trace.TraceInformation(result.Status);
|
||||
// Twilio doesn't currently have an async API, so return success.
|
||||
|
||||
return Task.FromResult(result.Status != "Failed");
|
||||
|
||||
} */
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
|
||||
@{
|
||||
ViewBag.Title = Localizer["Account"];
|
||||
}
|
||||
|
||||
|
||||
<div class="container">
|
||||
<div class="lead">
|
||||
<h1>Access Denied</h1>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
@model ForgotPasswordViewModel
|
||||
@{
|
||||
ViewBag.Title = Localizer["Account"];
|
||||
}
|
||||
|
||||
<form asp-controller="Account" asp-action="ForgotPassword">
|
||||
<div class="form-group">
|
||||
|
|
@ -6,4 +9,4 @@
|
|||
<input class="form-control" placeholder="LoginOrEmail" asp-for="LoginOrEmail" autofocus>
|
||||
</div>
|
||||
<button class="btn btn-primary" name="button" value="send">Send me a recovery link</button>
|
||||
</form>
|
||||
</form>
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
@model string
|
||||
|
||||
@{
|
||||
ViewBag.Title = Localizer["Account"];
|
||||
}
|
||||
|
||||
<h1>Check your mail box!</h1>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
@model LoggedOutViewModel
|
||||
@{
|
||||
ViewBag.Title = Localizer["Account"];
|
||||
}
|
||||
|
||||
@model LoggedOutViewModel
|
||||
|
||||
@{
|
||||
// set this so the layout rendering sees an anonymous user
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
@model LoginViewModel
|
||||
@{
|
||||
ViewBag.Title = Localizer["Account"];
|
||||
}
|
||||
|
||||
<div class="login-page">
|
||||
<div class="lead">
|
||||
|
|
@ -84,4 +87,4 @@
|
|||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
@model LogoutViewModel
|
||||
@{
|
||||
ViewBag.Title = Localizer["Account"];
|
||||
}
|
||||
|
||||
@model LogoutViewModel
|
||||
|
||||
<div class="logout-page">
|
||||
<div class="lead">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
@model RegisterModel
|
||||
@{
|
||||
ViewBag.Title = Localizer["Account"];
|
||||
}
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
<form method="post">
|
||||
@Html.EditorForModel()
|
||||
<button class="btn btn-primary" name="button"
|
||||
value="Register">Register</button>
|
||||
</form>
|
||||
</form>
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
@model ResetPasswordViewModel
|
||||
@{
|
||||
ViewBag.Title = Localizer["Account"];
|
||||
}
|
||||
|
||||
<form asp-route-id="@Model.Id" asp-route-code="@Model.Code">
|
||||
<p>Your email : <code>@Model.Email</code></p>
|
||||
|
|
@ -15,4 +18,4 @@
|
|||
|
||||
<button class="btn btn-primary" name="button" value="Reset">Reset Password</button>
|
||||
|
||||
</form>
|
||||
</form>
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
@model SignInModel
|
||||
@{
|
||||
ViewBag.Title = Localizer["Account"];
|
||||
}
|
||||
|
||||
<div class="login-page">
|
||||
<div class="lead">
|
||||
|
|
@ -84,4 +87,4 @@
|
|||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
@model HaircutAdminViewModel
|
||||
@{
|
||||
ViewBag.Title = Localizer["Administration"];
|
||||
}
|
||||
|
||||
<a asp-controller="HairTaints" class="btn btn-primary">
|
||||
Gestion des couleurs
|
||||
|
|
|
|||
|
|
@ -21,7 +21,19 @@
|
|||
@foreach (var user in Model.Users) {
|
||||
<tr>
|
||||
<td>
|
||||
<img src="~/avatars/@(user.UserName).xs.png" alt="avatar"/>
|
||||
@if (user.UserId==User.GetUserId()) {
|
||||
<span class="badge badge-warning">You</span>
|
||||
}
|
||||
@if (SiteSettings.Value.Admin.EMail == user.Email) {
|
||||
<span class="badge badge-success">Admin</span>
|
||||
}
|
||||
@if (SiteSettings.Value.Owner.EMail == user.Email) {
|
||||
<span class="badge badge-success">Owner</span>
|
||||
}
|
||||
@if (!String.IsNullOrWhiteSpace(user.Avatar))
|
||||
{
|
||||
<img src="@user.Avatar" alt="avatar" class="user-icon"/>
|
||||
}
|
||||
<span> @user.UserName <@(user.Email)> </span>
|
||||
</td>
|
||||
<td>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,9 @@
|
|||
@model IdentityServer8.EntityFramework.Entities.ApiScope
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
ViewBag.Title = Localizer["ApiScope"];
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Create</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h4>ApiScope</h4>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form asp-action="Create">
|
||||
|
|
@ -64,6 +53,3 @@
|
|||
<div>
|
||||
<a asp-action="Index">Back to List</a>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,8 @@
|
|||
@model Yavsc.Models.YavscApiScope
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
ViewBag.Title = Localizer["ApiScope"];
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Delete</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h3>Are you sure you want to delete this?</h3>
|
||||
<div>
|
||||
<h4>YavscApiScope</h4>
|
||||
|
|
@ -61,12 +51,9 @@
|
|||
@Html.DisplayFor(model => model.ShowInDiscoveryDocument)
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
|
||||
<form asp-action="Delete">
|
||||
<input type="hidden" asp-for="Id" />
|
||||
<input type="submit" value="Delete" class="btn btn-danger" /> |
|
||||
<a asp-action="Index">Back to List</a>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,8 @@
|
|||
@model Yavsc.Models.YavscApiScope
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
ViewBag.Title = Localizer["ApiScope"];
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Details</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div>
|
||||
<h4>YavscApiScope</h4>
|
||||
|
|
@ -65,5 +56,3 @@
|
|||
<a asp-action="Edit" asp-route-id="@Model?.Id">Edit</a> |
|
||||
<a asp-action="Index">Back to List</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,8 @@
|
|||
@model Yavsc.Models.YavscApiScope
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
ViewBag.Title = Localizer["ApiScope"];
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Edit</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h4>YavscApiScope</h4>
|
||||
<hr />
|
||||
|
|
@ -65,6 +56,3 @@
|
|||
<div>
|
||||
<a asp-action="Index">Back to List</a>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,10 @@
|
|||
@model IEnumerable<Yavsc.Models.YavscApiScope>
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
ViewBag.Title = Localizer["ApiScope"];
|
||||
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Index</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
<a asp-action="Create">Create New</a>
|
||||
</p>
|
||||
|
|
@ -75,5 +68,3 @@
|
|||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
@model Client
|
||||
@{
|
||||
ViewBag.Title = Localizer["Client"];
|
||||
}
|
||||
|
||||
<h2>@Localizer["Create"]</h2>
|
||||
|
||||
|
|
@ -77,4 +80,3 @@
|
|||
<div>
|
||||
<a asp-action="Index">@Localizer["Back to List"]</a>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
@model Client
|
||||
@{
|
||||
ViewBag.Title = Localizer["Client"];
|
||||
}
|
||||
|
||||
<h2>@Localizer["Delete"]</h2>
|
||||
|
||||
|
|
@ -63,4 +66,4 @@
|
|||
<a asp-action="Index">@Localizer["Back to List"]</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2,7 +2,12 @@
|
|||
@using Microsoft.AspNetCore.Html
|
||||
@using System.Text
|
||||
|
||||
@{
|
||||
ViewBag.Title = Localizer["Client"];
|
||||
}
|
||||
|
||||
@functions {
|
||||
|
||||
// Lifetimes in IdentityServer are stored as seconds (int). Render them
|
||||
// in a human-friendly way: "5 min", "2 h", "30 d", "—" for 0/negative.
|
||||
// Zero typically means "use the global default" — show it as such so
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
@model Client
|
||||
@{
|
||||
ViewBag.Title = Localizer["Client"];
|
||||
}
|
||||
|
||||
<h2>@Localizer["Edit"]</h2>
|
||||
|
||||
|
|
@ -303,4 +306,4 @@
|
|||
|
||||
<div>
|
||||
<a asp-action="Index">@Localizer["Back to List"]</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientClaim>
|
||||
@{
|
||||
ViewBag.Title = Localizer["Client"];
|
||||
}
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit Client Claims";
|
||||
var clientId = Model.FirstOrDefault()?.ClientId ?? 0;
|
||||
|
|
@ -61,4 +65,4 @@
|
|||
|
||||
<p>
|
||||
<a asp-action="Edit" asp-route-id="@clientId">Back to client</a>
|
||||
</p>
|
||||
</p>
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientCorsOrigin>
|
||||
@{
|
||||
ViewBag.Title = Localizer["Client"];
|
||||
}
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit Allowed CORS Origins";
|
||||
ViewData["addAction"] = "AddCorsOrigin";
|
||||
|
|
@ -23,4 +27,4 @@
|
|||
|
||||
<p>
|
||||
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
||||
</p>
|
||||
</p>
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientGrantType>
|
||||
@{
|
||||
ViewBag.Title = Localizer["Client"];
|
||||
}
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit Allowed Grant Types";
|
||||
ViewData["addAction"] = "AddGrantType";
|
||||
|
|
@ -24,4 +28,4 @@
|
|||
|
||||
<p>
|
||||
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
||||
</p>
|
||||
</p>
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientIdPRestriction>
|
||||
@{
|
||||
ViewBag.Title = Localizer["Client"];
|
||||
}
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit Identity Provider Restrictions";
|
||||
ViewData["addAction"] = "AddIdPRestriction";
|
||||
|
|
@ -21,4 +25,4 @@
|
|||
|
||||
<p>
|
||||
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
||||
</p>
|
||||
</p>
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri>
|
||||
@{
|
||||
ViewBag.Title = Localizer["Client"];
|
||||
}
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit Post-logout Redirect URIs";
|
||||
ViewData["addAction"] = "AddPostLogoutRedirectUri";
|
||||
|
|
@ -22,4 +26,4 @@
|
|||
|
||||
<p>
|
||||
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
||||
</p>
|
||||
</p>
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientProperty>
|
||||
@{
|
||||
ViewBag.Title = Localizer["Client"];
|
||||
}
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit Client Properties";
|
||||
var clientId = Model.FirstOrDefault()?.ClientId ?? 0;
|
||||
|
|
@ -62,4 +66,4 @@
|
|||
|
||||
<p>
|
||||
<a asp-action="Edit" asp-route-id="@clientId">Back to client</a>
|
||||
</p>
|
||||
</p>
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientRedirectUri>
|
||||
@{
|
||||
ViewBag.Title = Localizer["Client"];
|
||||
}
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit Redirect URIs";
|
||||
ViewData["addAction"] = "AddRedirectUri";
|
||||
|
|
@ -22,4 +26,4 @@
|
|||
|
||||
<p>
|
||||
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
||||
</p>
|
||||
</p>
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientScope>
|
||||
@{
|
||||
ViewBag.Title = Localizer["Client"];
|
||||
}
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit Allowed Scopes";
|
||||
ViewData["addAction"] = "AddScope";
|
||||
|
|
@ -22,4 +26,4 @@
|
|||
|
||||
<p>
|
||||
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
||||
</p>
|
||||
</p>
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientSecret>
|
||||
@{
|
||||
ViewBag.Title = Localizer["Client"];
|
||||
}
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit Client Secrets";
|
||||
var clientId = Model.FirstOrDefault()?.ClientId ?? 0;
|
||||
|
|
@ -80,4 +84,4 @@
|
|||
<p>
|
||||
<a asp-action="Edit" asp-route-id="@clientId">Back to client</a>
|
||||
<a asp-action="RegenerateSecret" asp-route-id="@clientId" class="ml-3">Regenerate a single secret</a>
|
||||
</p>
|
||||
</p>
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
@using IdentityServer8.Models;
|
||||
@model IEnumerable<IdentityServer8.EntityFramework.Entities.Client>
|
||||
|
||||
@{
|
||||
ViewBag.Title = Localizer["Client"];
|
||||
}
|
||||
|
||||
<h2>@Localizer["Index"]</h2>
|
||||
|
||||
<p>
|
||||
|
|
@ -10,7 +14,7 @@
|
|||
@foreach (var item in Model)
|
||||
{
|
||||
<div class="list-item">
|
||||
|
||||
|
||||
<div class="card">
|
||||
<h5 class="card-title">Identifier</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">and activation</h6>
|
||||
|
|
@ -36,7 +40,7 @@
|
|||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card">
|
||||
|
||||
<h5 class="card-title">Urls</h5>
|
||||
|
|
@ -56,7 +60,7 @@
|
|||
</dd>
|
||||
<dt>@Html.DisplayNameFor(model => model.AbsoluteRefreshTokenLifetime)</dt>
|
||||
<dd>@Html.DisplayFor(model => item.AbsoluteRefreshTokenLifetime)</dd>
|
||||
|
||||
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
|
|
@ -78,7 +82,7 @@
|
|||
</dt>
|
||||
<dd>
|
||||
@Enum.GetName(typeof(AccessTokenType), item.AccessTokenType)
|
||||
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
|
@ -86,6 +90,6 @@
|
|||
<a class="btn btn-primary btn-lg" asp-action="Edit" asp-route-id="@item.Id">Edit</a>
|
||||
<a class="btn btn-secondary btn-lg" asp-action="Details" asp-route-id="@item.Id">Details</a>
|
||||
<a class="btn btn-danger btn-lg" asp-action="Delete" asp-route-id="@item.Id">Delete</a>
|
||||
|
||||
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
@model Client
|
||||
@{
|
||||
ViewBag.Title = Localizer["Client"];
|
||||
}
|
||||
|
||||
<h2>@Localizer["Regenerate client secret"]</h2>
|
||||
|
||||
|
|
@ -36,4 +39,4 @@
|
|||
@Html.AntiForgeryToken()
|
||||
<button type="submit" class="btn btn-danger">@Localizer["Regenerate now"]</button>
|
||||
<a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-default">@Localizer["Cancel"]</a>
|
||||
</form>
|
||||
</form>
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
@{
|
||||
ViewBag.Title = Localizer["Client"];
|
||||
}
|
||||
|
||||
@{
|
||||
var secret = ViewBag.NewClientSecret as string;
|
||||
var expiresAt = ViewBag.NewClientSecretExpiresAt as string;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
@model IEnumerable<CommandForm>
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Index";
|
||||
ViewBag.Title = Localizer["CommandForms"];
|
||||
}
|
||||
|
||||
<h2>Index</h2>
|
||||
<h2>@ViewBag.Title</h2>
|
||||
|
||||
<p>
|
||||
<a asp-action="Create">Create New</a>
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
|
||||
@foreach (var item in Model) {
|
||||
<tr class="@(item.Context.Hidden?"disabled":null)">
|
||||
<td>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
@{
|
||||
ViewBag.Title = Localizer["Device"];
|
||||
}
|
||||
|
||||
<div class="page-device-success">
|
||||
<div class="lead">
|
||||
<h1>Success</h1>
|
||||
<p>You have successfully authorized the device</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
@model string
|
||||
@{
|
||||
ViewBag.Title = Localizer["Device"];
|
||||
}
|
||||
|
||||
<div class="page-device-code">
|
||||
<div class="lead">
|
||||
|
|
@ -20,4 +23,4 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
@model DeviceAuthorizationViewModel
|
||||
@{
|
||||
ViewBag.Title = Localizer["Device"];
|
||||
}
|
||||
|
||||
<div class="page-device-confirmation">
|
||||
<div class="lead">
|
||||
|
|
@ -105,4 +108,4 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
@model DiagnosticsViewModel
|
||||
|
||||
@{
|
||||
ViewBag.Title = Localizer["Diagnostics"];
|
||||
}
|
||||
|
||||
<div class="diagnostics-page">
|
||||
<div class="lead">
|
||||
<h1>Authentication Cookie</h1>
|
||||
|
|
@ -22,7 +26,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
@model Instrumentation
|
||||
@{
|
||||
ViewBag.Title = Localizer["Do"];
|
||||
}
|
||||
|
||||
<h2>@Model.Tool.Name</h2>
|
||||
|
||||
|
|
@ -6,4 +9,4 @@
|
|||
Remove Instrument</a>
|
||||
<form asp-action="AddInstrument" asp-controller="Instrumentation" asp-action-id="Model.UserId" >
|
||||
<select name="Name" value="" placeholder="Séléctionnez votre instrument" ></select>
|
||||
</form>
|
||||
</form>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
@{
|
||||
ViewBag.Title = Localizer["FrontOffice"];
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
@{
|
||||
ViewBag.Title = Localizer["FrontOffice"];
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
@{
|
||||
ViewBag.Title = Localizer["FrontOffice"];
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
@{
|
||||
ViewBag.Title = Localizer["FrontOffice"];
|
||||
}
|
||||
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
<dt>EstimateToSignAsPro
|
||||
</dt>
|
||||
<dd>@Html.DisplayFor(m=>m.EstimateToSignAsProCount)
|
||||
<dd>@Html.DisplayFor(m=>m.EstimateToHonorAsProCount)
|
||||
</dd>
|
||||
|
||||
<dt>EstimateToSignAsCli
|
||||
|
|
@ -21,11 +21,6 @@
|
|||
<dd>@Html.DisplayFor(m=>m.EstimateToSignAsCliCount)
|
||||
</dd>
|
||||
|
||||
<dt>BillToSignAsPro
|
||||
</dt>
|
||||
<dd>@Html.DisplayFor(m=>m.BillToSignAsProCount)
|
||||
</dd>
|
||||
|
||||
<dt>BillToSignAsCli
|
||||
</dt>
|
||||
<dd>@Html.DisplayFor(m=>m.BillToSignAsCliCount)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
@model Yavsc.Models.Haircut.BrusherProfile
|
||||
@{
|
||||
ViewBag.Title = Localizer["FrontOffice"];
|
||||
}
|
||||
|
||||
<fieldset>
|
||||
<legend>Disponibilités</legend>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue