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",
|
"cSpell.language": "fr,en",
|
||||||
"makefile.configureOnOpen": false,
|
"makefile.configureOnOpen": false,
|
||||||
"search.useGlobalIgnoreFiles": true,
|
"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>
|
<ItemGroup>
|
||||||
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
|
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
|
||||||
<PackageVersion Include="GitVersion.MsBuild" Version="6.7.0" />
|
<PackageVersion Include="GitVersion.MsBuild" Version="6.8.1" />
|
||||||
<PackageVersion Include="HigginsSoft.IdentityServer8" Version="8.0.5-preview-net9" />
|
<PackageVersion Include="HigginsSoft.IdentityServer8" Version="8.1.0-alpha.171" />
|
||||||
<PackageVersion Include="HigginsSoft.IdentityServer8.EntityFramework" Version="8.0.5-preview-net9" />
|
<PackageVersion Include="HigginsSoft.IdentityServer8.EntityFramework" Version="8.1.0-alpha.171" />
|
||||||
<PackageVersion Include="IdentityModel.OidcClient" Version="6.0.0" />
|
<PackageVersion Include="IdentityModel.OidcClient" Version="6.0.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.9" />
|
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.9" />
|
||||||
|
|
@ -24,10 +24,10 @@
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.9" />
|
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.9" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
|
||||||
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="10.0.2" />
|
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="10.0.2" />
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||||
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
||||||
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
|
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ COPY src/PostIt/PostIt.Desktop/*.csproj ./src/PostIt/PostIt.Desktop/
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# 3. Restauration des dépendances avec vos workloads actifs
|
# 3. Restauration des dépendances avec vos workloads actifs
|
||||||
RUN dotnet nuget add source https://isn.pschneider.fr/v3/index.json --allow-insecure-connections
|
RUN dotnet nuget add source https://isn.pschneider.fr/api/v3/index.json
|
||||||
|
|
||||||
# 4. Restauration des dépendances pour tous les projets
|
# 4. Restauration des dépendances pour tous les projets
|
||||||
RUN dotnet restore
|
RUN dotnet restore
|
||||||
|
|
|
||||||
6
Makefile
6
Makefile
|
|
@ -10,8 +10,8 @@ include .env
|
||||||
all:
|
all:
|
||||||
dotnet build --nologo
|
dotnet build --nologo
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
dotnet clean
|
dotnet clean -c $(CONFIG)
|
||||||
|
|
||||||
src/Yavsc/bin/output/wwwroot:
|
src/Yavsc/bin/output/wwwroot:
|
||||||
dotnet --project src/Yavsc.Org/Yavsc.Org.csproj publish
|
dotnet --project src/Yavsc.Org/Yavsc.Org.csproj publish
|
||||||
|
|
@ -31,7 +31,7 @@ src/Yavsc.Server/bin/$(CONFIG)/$(FRAMEWORK)/Yavsc.Server.dll:
|
||||||
src/Yavsc/bin/$(CONFIG)/$(FRAMEWORK)/Yavsc.dll:
|
src/Yavsc/bin/$(CONFIG)/$(FRAMEWORK)/Yavsc.dll:
|
||||||
dotnet build -p:Configuration=$(CONFIG) --project src/Yavsc.Org/Yavsc.Org.csproj
|
dotnet build -p:Configuration=$(CONFIG) --project src/Yavsc.Org/Yavsc.Org.csproj
|
||||||
|
|
||||||
$(DESTDIR):
|
$(DESTDIR):
|
||||||
mkdir $(DESTDIR)
|
mkdir $(DESTDIR)
|
||||||
|
|
||||||
install: $(DESTDIR)
|
install: $(DESTDIR)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ namespace PostIt.Tests;
|
||||||
/// URL emitted by OidcClient, extracts its <c>state</c>, and returns a
|
/// URL emitted by OidcClient, extracts its <c>state</c>, and returns a
|
||||||
/// BrowserResult that mimics the OIDC redirect-with-code callback.
|
/// BrowserResult that mimics the OIDC redirect-with-code callback.
|
||||||
///
|
///
|
||||||
/// The paired <see cref="OidcStubAuthority"/>'s token endpoint accepts
|
/// The paired <see cref="OIDCStubAuthority"/>'s token endpoint accepts
|
||||||
/// any authorization code, so we don't need to mint a real one here.
|
/// any authorization code, so we don't need to mint a real one here.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class FakeAuthorizingBrowser
|
public sealed class FakeAuthorizingBrowser
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ public class LoginPageViewModelTests
|
||||||
// short-circuits the system browser. The authority signs its
|
// short-circuits the system browser. The authority signs its
|
||||||
// access_token with RS256; the fake browser captures the redirect
|
// access_token with RS256; the fake browser captures the redirect
|
||||||
// URI so the authority can complete the token exchange.
|
// URI so the authority can complete the token exchange.
|
||||||
using var authority = await OidcStubAuthority.StartAsync();
|
using var authority = await OIDCStubAuthority.StartAsync();
|
||||||
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
|
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
|
||||||
|
|
||||||
var settings = new PostIt.Settings
|
var settings = new PostIt.Settings
|
||||||
|
|
@ -96,7 +96,7 @@ public class LoginPageViewModelTests
|
||||||
// double slash before /.well-known/openid-configuration. The
|
// double slash before /.well-known/openid-configuration. The
|
||||||
// stub advertises itself without the trailing slash; OidcClient
|
// stub advertises itself without the trailing slash; OidcClient
|
||||||
// must bridge.
|
// must bridge.
|
||||||
using var authority = await OidcStubAuthority.StartAsync();
|
using var authority = await OIDCStubAuthority.StartAsync();
|
||||||
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
|
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
|
||||||
|
|
||||||
var settings = new PostIt.Settings
|
var settings = new PostIt.Settings
|
||||||
|
|
@ -254,4 +254,4 @@ public class LoginPageViewModelTests
|
||||||
"https://yavsc.example.com/.well-known/openid-configuration",
|
"https://yavsc.example.com/.well-known/openid-configuration",
|
||||||
vm.StatusMessage);
|
vm.StatusMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ namespace PostIt.Tests;
|
||||||
/// the browser intercepts the authorize redirect, the server completes
|
/// the browser intercepts the authorize redirect, the server completes
|
||||||
/// the token exchange.
|
/// the token exchange.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable
|
public sealed class OIDCStubAuthority : IAsyncDisposable, IDisposable
|
||||||
{
|
{
|
||||||
private readonly HttpListener _listener;
|
private readonly HttpListener _listener;
|
||||||
private readonly RSA _rsa;
|
private readonly RSA _rsa;
|
||||||
|
|
@ -30,7 +30,7 @@ public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable
|
||||||
public string Issuer { get; }
|
public string Issuer { get; }
|
||||||
public string LoopbackRedirectUri { get; }
|
public string LoopbackRedirectUri { get; }
|
||||||
|
|
||||||
private OidcStubAuthority(HttpListener listener, RSA rsa, string kid, string issuer, string loopback)
|
private OIDCStubAuthority(HttpListener listener, RSA rsa, string kid, string issuer, string loopback)
|
||||||
{
|
{
|
||||||
_listener = listener;
|
_listener = listener;
|
||||||
_rsa = rsa;
|
_rsa = rsa;
|
||||||
|
|
@ -39,7 +39,7 @@ public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable
|
||||||
LoopbackRedirectUri = loopback;
|
LoopbackRedirectUri = loopback;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<OidcStubAuthority> StartAsync()
|
public static async Task<OIDCStubAuthority> StartAsync()
|
||||||
{
|
{
|
||||||
// Pick a free loopback port.
|
// Pick a free loopback port.
|
||||||
var port = GetFreePort();
|
var port = GetFreePort();
|
||||||
|
|
@ -53,7 +53,7 @@ public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable
|
||||||
var rsa = RSA.Create(2048);
|
var rsa = RSA.Create(2048);
|
||||||
var kid = "test-key-1";
|
var kid = "test-key-1";
|
||||||
|
|
||||||
var authority = new OidcStubAuthority(listener, rsa, kid, prefix.TrimEnd('/'), loopback);
|
var authority = new OIDCStubAuthority(listener, rsa, kid, prefix.TrimEnd('/'), loopback);
|
||||||
_ = Task.Run(() => authority.AcceptLoopAsync(authority._cts.Token));
|
_ = Task.Run(() => authority.AcceptLoopAsync(authority._cts.Token));
|
||||||
return authority;
|
return authority;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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
|
/// End-to-end coverage of <see cref="YavscApiClient"/>: silent
|
||||||
/// refresh on a near-expiry access token, 401-driven refresh + retry,
|
/// refresh on a near-expiry access token, 401-driven refresh + retry,
|
||||||
/// and persistence of the token bundle via <see cref="TokenStore"/>.
|
/// and persistence of the token bundle via <see cref="TokenStore"/>.
|
||||||
/// Uses the project's <see cref="OidcStubAuthority"/> for the IdP and
|
/// Uses the project's <see cref="OIDCStubAuthority"/> for the IdP and
|
||||||
/// a tiny in-process HTTP listener for the API server side.
|
/// a tiny in-process HTTP listener for the API server side.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class YavscApiClientTests
|
public class YavscApiClientTests
|
||||||
|
|
@ -45,7 +45,7 @@ public class YavscApiClientTests
|
||||||
// in-memory access token as expired and re-run a call. The
|
// in-memory access token as expired and re-run a call. The
|
||||||
// refresh path must rotate the refresh token transparently
|
// refresh path must rotate the refresh token transparently
|
||||||
// and the API call must succeed with the new token.
|
// and the API call must succeed with the new token.
|
||||||
using var authority = await OidcStubAuthority.StartAsync();
|
using var authority = await OIDCStubAuthority.StartAsync();
|
||||||
using var apiServer = new StubApiServer();
|
using var apiServer = new StubApiServer();
|
||||||
await apiServer.StartAsync();
|
await apiServer.StartAsync();
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ public class YavscApiClientTests
|
||||||
var reloaded = new YavscApiClient(settings, new TokenStore(tokensPath));
|
var reloaded = new YavscApiClient(settings, new TokenStore(tokensPath));
|
||||||
|
|
||||||
var posts = await reloaded.CallAsync<List<StubApiServer.Post>>(
|
var posts = await reloaded.CallAsync<List<StubApiServer.Post>>(
|
||||||
HttpMethod.Get, "posts");
|
HttpMethod.Get, "posts", TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
Assert.NotNull(posts);
|
Assert.NotNull(posts);
|
||||||
Assert.NotEmpty(posts);
|
Assert.NotEmpty(posts);
|
||||||
|
|
@ -85,7 +85,7 @@ public class YavscApiClientTests
|
||||||
{
|
{
|
||||||
// API server returns 401 on the first request, 200 on the next.
|
// API server returns 401 on the first request, 200 on the next.
|
||||||
// YavscApiClient must refresh, then retry exactly once.
|
// YavscApiClient must refresh, then retry exactly once.
|
||||||
using var authority = await OidcStubAuthority.StartAsync();
|
using var authority = await OIDCStubAuthority.StartAsync();
|
||||||
using var apiServer = new StubApiServer(forceFirstRequest: true);
|
using var apiServer = new StubApiServer(forceFirstRequest: true);
|
||||||
await apiServer.StartAsync();
|
await apiServer.StartAsync();
|
||||||
|
|
||||||
|
|
@ -97,7 +97,7 @@ public class YavscApiClientTests
|
||||||
settings, authority, tokensPath);
|
settings, authority, tokensPath);
|
||||||
|
|
||||||
var posts = await client.CallAsync<List<StubApiServer.Post>>(
|
var posts = await client.CallAsync<List<StubApiServer.Post>>(
|
||||||
HttpMethod.Get, "posts");
|
HttpMethod.Get, "posts", TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
Assert.NotEmpty(posts);
|
Assert.NotEmpty(posts);
|
||||||
Assert.Equal(2, apiServer.RequestCount);
|
Assert.Equal(2, apiServer.RequestCount);
|
||||||
|
|
@ -125,14 +125,15 @@ public class YavscApiClientTests
|
||||||
var client = new YavscApiClient(settings, new TokenStore(Path.Combine(
|
var client = new YavscApiClient(settings, new TokenStore(Path.Combine(
|
||||||
Path.GetTempPath(), $"postit-tests-noop-{Guid.NewGuid():N}.json")));
|
Path.GetTempPath(), $"postit-tests-noop-{Guid.NewGuid():N}.json")));
|
||||||
|
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
client.CallAsync<JsonElement>(HttpMethod.Get, "posts"));
|
() =>
|
||||||
|
client.CallAsync<JsonElement>(HttpMethod.Get, "posts", TestContext.Current.CancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HasValidSession_is_true_after_login()
|
public async Task HasValidSession_is_true_after_login()
|
||||||
{
|
{
|
||||||
using var authority = await OidcStubAuthority.StartAsync();
|
using var authority = await OIDCStubAuthority.StartAsync();
|
||||||
using var apiServer = new StubApiServer();
|
using var apiServer = new StubApiServer();
|
||||||
await apiServer.StartAsync();
|
await apiServer.StartAsync();
|
||||||
|
|
||||||
|
|
@ -154,7 +155,7 @@ public class YavscApiClientTests
|
||||||
|
|
||||||
// --- helpers --------------------------------------------------------
|
// --- helpers --------------------------------------------------------
|
||||||
|
|
||||||
private static PostIt.Settings BuildSettings(OidcStubAuthority authority, string apiBaseUrl) => new()
|
private static PostIt.Settings BuildSettings(OIDCStubAuthority authority, string apiBaseUrl) => new()
|
||||||
{
|
{
|
||||||
Authentication = new AuthenticationSettings
|
Authentication = new AuthenticationSettings
|
||||||
{
|
{
|
||||||
|
|
@ -167,7 +168,7 @@ public class YavscApiClientTests
|
||||||
};
|
};
|
||||||
|
|
||||||
private static async Task<YavscApiClient> LoginAndPersistAsync(
|
private static async Task<YavscApiClient> LoginAndPersistAsync(
|
||||||
PostIt.Settings settings, OidcStubAuthority authority, string tokensPath)
|
PostIt.Settings settings, OIDCStubAuthority authority, string tokensPath)
|
||||||
{
|
{
|
||||||
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
|
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
|
||||||
var client = new YavscApiClient(settings, new TokenStore(tokensPath));
|
var client = new YavscApiClient(settings, new TokenStore(tokensPath));
|
||||||
|
|
@ -181,7 +182,7 @@ public class YavscApiClientTests
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// YavscApiClient.LoginInteractiveAsync delegates to
|
/// YavscApiClient.LoginInteractiveAsync delegates to
|
||||||
/// Platform.CreateBrowser. We can't override that static cleanly
|
/// Platform.CreateBrowser. We can't override that static cleanly
|
||||||
/// from xunit.v3, so we rebuild the call by re-routing the
|
/// from XUnit.v3, so we rebuild the call by re-routing the
|
||||||
/// Platform.CreateBrowser delegate for the duration of the call.
|
/// Platform.CreateBrowser delegate for the duration of the call.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task LoginWithBrowserAsync(
|
private static async Task LoginWithBrowserAsync(
|
||||||
|
|
@ -214,7 +215,7 @@ public class YavscApiClientTests
|
||||||
File.WriteAllText(tokensPath, JsonSerializer.Serialize(record));
|
File.WriteAllText(tokensPath, JsonSerializer.Serialize(record));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- OidcLoginPhase progress tests ---------------------------------
|
// --- OIDCLoginPhase progress tests ---------------------------------
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Collecting Progress<T> is documented to capture reports
|
/// Collecting Progress<T> is documented to capture reports
|
||||||
|
|
@ -225,7 +226,7 @@ public class YavscApiClientTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task LoginInteractiveAsync_reports_Discovering_then_Success()
|
public async Task LoginInteractiveAsync_reports_Discovering_then_Success()
|
||||||
{
|
{
|
||||||
using var authority = await OidcStubAuthority.StartAsync();
|
using var authority = await OIDCStubAuthority.StartAsync();
|
||||||
using var apiServer = new StubApiServer();
|
using var apiServer = new StubApiServer();
|
||||||
await apiServer.StartAsync();
|
await apiServer.StartAsync();
|
||||||
|
|
||||||
|
|
@ -234,18 +235,18 @@ public class YavscApiClientTests
|
||||||
var client = new YavscApiClient(settings, new TokenStore(tokensPath));
|
var client = new YavscApiClient(settings, new TokenStore(tokensPath));
|
||||||
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
|
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
|
||||||
|
|
||||||
var reported = new System.Collections.Generic.List<OidcLoginPhase>();
|
var reported = new System.Collections.Generic.List<OIDCLoginPhase>();
|
||||||
var progress = new SyncProgress<OidcLoginPhase>(reported);
|
var progress = new SyncProgress<OIDCLoginPhase>(reported);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await LoginWithBrowserAsync(client, browser.CreateBrowser(), progress);
|
await LoginWithBrowserAsync(client, browser.CreateBrowser(), progress);
|
||||||
// SyncProgress captures reports synchronously — no flush needed.
|
// SyncProgress captures reports synchronously — no flush needed.
|
||||||
|
|
||||||
Assert.Contains(OidcLoginPhase.Discovering, reported);
|
Assert.Contains(OIDCLoginPhase.Discovering, reported);
|
||||||
Assert.Contains(OidcLoginPhase.OpeningBrowser, reported);
|
Assert.Contains(OIDCLoginPhase.OpeningBrowser, reported);
|
||||||
Assert.Contains(OidcLoginPhase.ExchangingCode, reported);
|
Assert.Contains(OIDCLoginPhase.ExchangingCode, reported);
|
||||||
Assert.Equal(OidcLoginPhase.Success, Last(reported));
|
Assert.Equal(OIDCLoginPhase.Success, Last(reported));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -256,24 +257,24 @@ public class YavscApiClientTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task LoginInteractiveAsync_reports_Error_when_browser_missing()
|
public async Task LoginInteractiveAsync_reports_Error_when_browser_missing()
|
||||||
{
|
{
|
||||||
using var authority = await OidcStubAuthority.StartAsync();
|
using var authority = await OIDCStubAuthority.StartAsync();
|
||||||
using var apiServer = new StubApiServer();
|
using var apiServer = new StubApiServer();
|
||||||
await apiServer.StartAsync();
|
await apiServer.StartAsync();
|
||||||
|
|
||||||
var settings = BuildSettings(authority, apiServer.BaseUrl);
|
var settings = BuildSettings(authority, apiServer.BaseUrl);
|
||||||
var client = new YavscApiClient(settings, new TokenStore(TokensPath()));
|
var client = new YavscApiClient(settings, new TokenStore(TokensPath()));
|
||||||
var reported = new System.Collections.Generic.List<OidcLoginPhase>();
|
var reported = new System.Collections.Generic.List<OIDCLoginPhase>();
|
||||||
var progress = new SyncProgress<OidcLoginPhase>(reported);
|
var progress = new SyncProgress<OIDCLoginPhase>(reported);
|
||||||
|
|
||||||
var original = Platform.CreateBrowser;
|
var original = Platform.CreateBrowser;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Platform.CreateBrowser = () => null; // simulate no browser wired up
|
Platform.CreateBrowser = () => null; // simulate no browser wired up
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
() => client.LoginInteractiveAsync(progress));
|
() => client.LoginInteractiveAsync(progress, TestContext.Current.CancellationToken));
|
||||||
// SyncProgress captures reports synchronously — no flush needed.
|
// SyncProgress captures reports synchronously — no flush needed.
|
||||||
|
|
||||||
Assert.Equal(OidcLoginPhase.Error, Last(reported));
|
Assert.Equal(OIDCLoginPhase.Error, Last(reported));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -284,7 +285,7 @@ public class YavscApiClientTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TrySilentLoginAsync_returns_false_when_no_bundle_on_disk()
|
public async Task TrySilentLoginAsync_returns_false_when_no_bundle_on_disk()
|
||||||
{
|
{
|
||||||
using var authority = await OidcStubAuthority.StartAsync();
|
using var authority = await OIDCStubAuthority.StartAsync();
|
||||||
using var apiServer = new StubApiServer();
|
using var apiServer = new StubApiServer();
|
||||||
await apiServer.StartAsync();
|
await apiServer.StartAsync();
|
||||||
|
|
||||||
|
|
@ -293,7 +294,7 @@ public class YavscApiClientTests
|
||||||
// Tokens file deliberately doesn't exist.
|
// Tokens file deliberately doesn't exist.
|
||||||
var client = new YavscApiClient(settings, new TokenStore(tokensPath));
|
var client = new YavscApiClient(settings, new TokenStore(tokensPath));
|
||||||
|
|
||||||
var ok = await client.TrySilentLoginAsync();
|
var ok = await client.TrySilentLoginAsync(null, TestContext.Current.CancellationToken);
|
||||||
Assert.False(ok);
|
Assert.False(ok);
|
||||||
Assert.False(client.HasValidSession);
|
Assert.False(client.HasValidSession);
|
||||||
}
|
}
|
||||||
|
|
@ -301,7 +302,7 @@ public class YavscApiClientTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TrySilentLoginAsync_returns_true_when_access_token_still_valid()
|
public async Task TrySilentLoginAsync_returns_true_when_access_token_still_valid()
|
||||||
{
|
{
|
||||||
using var authority = await OidcStubAuthority.StartAsync();
|
using var authority = await OIDCStubAuthority.StartAsync();
|
||||||
using var apiServer = new StubApiServer();
|
using var apiServer = new StubApiServer();
|
||||||
await apiServer.StartAsync();
|
await apiServer.StartAsync();
|
||||||
|
|
||||||
|
|
@ -314,7 +315,7 @@ public class YavscApiClientTests
|
||||||
{
|
{
|
||||||
await LoginWithBrowserAsync(client, browser.CreateBrowser());
|
await LoginWithBrowserAsync(client, browser.CreateBrowser());
|
||||||
// Login fresh → access token is far from expiry.
|
// Login fresh → access token is far from expiry.
|
||||||
var ok = await client.TrySilentLoginAsync();
|
var ok = await client.TrySilentLoginAsync(null, TestContext.Current.CancellationToken);
|
||||||
Assert.True(ok);
|
Assert.True(ok);
|
||||||
Assert.True(client.HasValidSession);
|
Assert.True(client.HasValidSession);
|
||||||
}
|
}
|
||||||
|
|
@ -327,7 +328,7 @@ public class YavscApiClientTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TrySilentLoginAsync_returns_true_when_refresh_succeeds()
|
public async Task TrySilentLoginAsync_returns_true_when_refresh_succeeds()
|
||||||
{
|
{
|
||||||
using var authority = await OidcStubAuthority.StartAsync();
|
using var authority = await OIDCStubAuthority.StartAsync();
|
||||||
using var apiServer = new StubApiServer();
|
using var apiServer = new StubApiServer();
|
||||||
await apiServer.StartAsync();
|
await apiServer.StartAsync();
|
||||||
|
|
||||||
|
|
@ -351,13 +352,13 @@ public class YavscApiClientTests
|
||||||
// matches the disk: access expired, refresh still good.
|
// matches the disk: access expired, refresh still good.
|
||||||
var client = new YavscApiClient(settings, store);
|
var client = new YavscApiClient(settings, store);
|
||||||
|
|
||||||
var reported = new System.Collections.Generic.List<OidcLoginPhase>();
|
var reported = new System.Collections.Generic.List<OIDCLoginPhase>();
|
||||||
var progress = new SyncProgress<OidcLoginPhase>(reported);
|
var progress = new SyncProgress<OIDCLoginPhase>(reported);
|
||||||
|
|
||||||
var ok = await client.TrySilentLoginAsync(progress);
|
var ok = await client.TrySilentLoginAsync(progress, TestContext.Current.CancellationToken);
|
||||||
Assert.True(ok, "silent refresh should succeed via the stub authority.");
|
Assert.True(ok, "silent refresh should succeed via the stub authority.");
|
||||||
Assert.Contains(OidcLoginPhase.ExchangingCode, reported);
|
Assert.Contains(OIDCLoginPhase.ExchangingCode, reported);
|
||||||
Assert.Equal(OidcLoginPhase.Success, Last(reported));
|
Assert.Equal(OIDCLoginPhase.Success, Last(reported));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -430,7 +431,7 @@ public class YavscApiClientTests
|
||||||
/// overload stays for tests that don't care about phase events.
|
/// overload stays for tests that don't care about phase events.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task LoginWithBrowserAsync(
|
private static async Task LoginWithBrowserAsync(
|
||||||
YavscApiClient client, IBrowser browser, IProgress<OidcLoginPhase>? progress = null)
|
YavscApiClient client, IBrowser browser, IProgress<OIDCLoginPhase>? progress = null)
|
||||||
{
|
{
|
||||||
var original = Platform.CreateBrowser;
|
var original = Platform.CreateBrowser;
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ public partial class App : Application
|
||||||
services.AddTransient<LoginPage>();
|
services.AddTransient<LoginPage>();
|
||||||
services.AddTransient<SettingsPage>();
|
services.AddTransient<SettingsPage>();
|
||||||
services.AddTransient<HomePage>();
|
services.AddTransient<HomePage>();
|
||||||
|
services.AddTransient<SignaturePage>();
|
||||||
|
|
||||||
// ViewModels
|
// ViewModels
|
||||||
services.AddSingleton(settings);
|
services.AddSingleton(settings);
|
||||||
|
|
@ -72,6 +73,7 @@ public partial class App : Application
|
||||||
services.AddTransient<SettingsPageViewModel>();
|
services.AddTransient<SettingsPageViewModel>();
|
||||||
services.AddTransient<LoginPageViewModel>();
|
services.AddTransient<LoginPageViewModel>();
|
||||||
services.AddTransient<HomePageViewModel>();
|
services.AddTransient<HomePageViewModel>();
|
||||||
|
services.AddTransient<SignaturePageViewModel>();
|
||||||
|
|
||||||
// Persistent session banner: one instance for the lifetime of
|
// Persistent session banner: one instance for the lifetime of
|
||||||
// the app so the same VM survives page navigation.
|
// 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
|
/// The set is deliberately small: each value is a milestone an
|
||||||
/// operator can grep for in logs / StatusMessage, not a heartbeat.
|
/// operator can grep for in logs / StatusMessage, not a heartbeat.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum OidcLoginPhase
|
public enum OIDCLoginPhase
|
||||||
{
|
{
|
||||||
/// <summary>No login in flight (or login has settled).</summary>
|
/// <summary>No login in flight (or login has settled).</summary>
|
||||||
Idle,
|
Idle,
|
||||||
|
|
|
||||||
|
|
@ -91,15 +91,15 @@ public class YavscApiClient : IAsyncDisposable
|
||||||
/// <see cref="LoginPageViewModel.StatusMessage"/> for the human
|
/// <see cref="LoginPageViewModel.StatusMessage"/> for the human
|
||||||
/// text (URLs, error detail).</param>
|
/// text (URLs, error detail).</param>
|
||||||
public async Task LoginInteractiveAsync(
|
public async Task LoginInteractiveAsync(
|
||||||
IProgress<OidcLoginPhase>? progress = null,
|
IProgress<OIDCLoginPhase>? progress = null,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
progress?.Report(OidcLoginPhase.Discovering);
|
progress?.Report(OIDCLoginPhase.Discovering);
|
||||||
|
|
||||||
var browser = Platform.CreateBrowser?.Invoke();
|
var browser = Platform.CreateBrowser?.Invoke();
|
||||||
if (browser is null)
|
if (browser is null)
|
||||||
{
|
{
|
||||||
progress?.Report(OidcLoginPhase.Error);
|
progress?.Report(OIDCLoginPhase.Error);
|
||||||
throw new InvalidOperationException("No browser is available on this platform.");
|
throw new InvalidOperationException("No browser is available on this platform.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,20 +114,20 @@ public class YavscApiClient : IAsyncDisposable
|
||||||
// the moment we ask the browser to open (covers the entire
|
// the moment we ask the browser to open (covers the entire
|
||||||
// user-driven window including the AwaitingCallback wait), and
|
// user-driven window including the AwaitingCallback wait), and
|
||||||
// the moment we trade the code for tokens.
|
// the moment we trade the code for tokens.
|
||||||
progress?.Report(OidcLoginPhase.OpeningBrowser);
|
progress?.Report(OIDCLoginPhase.OpeningBrowser);
|
||||||
var result = await client.LoginAsync(new LoginRequest(), ct).ConfigureAwait(false);
|
var result = await client.LoginAsync(new LoginRequest(), ct).ConfigureAwait(false);
|
||||||
|
|
||||||
if (result.IsError)
|
if (result.IsError)
|
||||||
{
|
{
|
||||||
progress?.Report(OidcLoginPhase.Error);
|
progress?.Report(OIDCLoginPhase.Error);
|
||||||
throw new InvalidOperationException($"OIDC login failed: {result.Error}");
|
throw new InvalidOperationException($"OIDC login failed: {result.Error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
progress?.Report(OidcLoginPhase.ExchangingCode);
|
progress?.Report(OIDCLoginPhase.ExchangingCode);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(result.RefreshToken))
|
if (string.IsNullOrEmpty(result.RefreshToken))
|
||||||
{
|
{
|
||||||
progress?.Report(OidcLoginPhase.Error);
|
progress?.Report(OIDCLoginPhase.Error);
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Missing refresh_token — vérifie le scope 'offline_access'.");
|
"Missing refresh_token — vérifie le scope 'offline_access'.");
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +139,7 @@ public class YavscApiClient : IAsyncDisposable
|
||||||
IdToken: result.IdentityToken);
|
IdToken: result.IdentityToken);
|
||||||
|
|
||||||
_store.Save(_tokens);
|
_store.Save(_tokens);
|
||||||
progress?.Report(OidcLoginPhase.Success);
|
progress?.Report(OIDCLoginPhase.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -152,7 +152,7 @@ public class YavscApiClient : IAsyncDisposable
|
||||||
/// phase and returns false so the UI can keep going.
|
/// phase and returns false so the UI can keep going.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<bool> TrySilentLoginAsync(
|
public async Task<bool> TrySilentLoginAsync(
|
||||||
IProgress<OidcLoginPhase>? progress = null,
|
IProgress<OIDCLoginPhase>? progress = null,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (!HasValidSession) return false;
|
if (!HasValidSession) return false;
|
||||||
|
|
@ -161,7 +161,7 @@ public class YavscApiClient : IAsyncDisposable
|
||||||
// Access token still has plenty of life — nothing to do.
|
// Access token still has plenty of life — nothing to do.
|
||||||
if (_tokens.AccessTokenExpiresAt - DateTimeOffset.UtcNow > RefreshSkew)
|
if (_tokens.AccessTokenExpiresAt - DateTimeOffset.UtcNow > RefreshSkew)
|
||||||
{
|
{
|
||||||
progress?.Report(OidcLoginPhase.Success);
|
progress?.Report(OIDCLoginPhase.Success);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,19 +172,19 @@ public class YavscApiClient : IAsyncDisposable
|
||||||
// the user back to the login page.
|
// the user back to the login page.
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
progress?.Report(OidcLoginPhase.ExchangingCode);
|
progress?.Report(OIDCLoginPhase.ExchangingCode);
|
||||||
await ForceRefreshAsync(ct).ConfigureAwait(false);
|
await ForceRefreshAsync(ct).ConfigureAwait(false);
|
||||||
progress?.Report(OidcLoginPhase.Success);
|
progress?.Report(OIDCLoginPhase.Success);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (RefreshFailedException)
|
catch (RefreshFailedException)
|
||||||
{
|
{
|
||||||
progress?.Report(OidcLoginPhase.Idle);
|
progress?.Report(OIDCLoginPhase.Idle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
progress?.Report(OidcLoginPhase.Idle);
|
progress?.Report(OIDCLoginPhase.Idle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -205,6 +205,16 @@ public class YavscApiClient : IAsyncDisposable
|
||||||
return dto!;
|
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>
|
/// <summary>Call an endpoint that returns no useful body (DELETE, etc.).</summary>
|
||||||
public async Task CallAsync(
|
public async Task CallAsync(
|
||||||
HttpMethod method,
|
HttpMethod method,
|
||||||
|
|
@ -216,6 +226,16 @@ public class YavscApiClient : IAsyncDisposable
|
||||||
response.EnsureSuccessStatusCode();
|
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(
|
private async Task<HttpResponseMessage> SendAsync(
|
||||||
HttpMethod method, string path, object? body, CancellationToken ct)
|
HttpMethod method, string path, object? body, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ public class ViewLocator : IDataTemplate
|
||||||
SettingsPageViewModel => _services.GetRequiredService<SettingsPage>(),
|
SettingsPageViewModel => _services.GetRequiredService<SettingsPage>(),
|
||||||
LoginPageViewModel => _services.GetRequiredService<LoginPage>(),
|
LoginPageViewModel => _services.GetRequiredService<LoginPage>(),
|
||||||
HomePageViewModel => _services.GetRequiredService<HomePage>(),
|
HomePageViewModel => _services.GetRequiredService<HomePage>(),
|
||||||
|
SignaturePageViewModel => _services.GetRequiredService<SignaturePage>(),
|
||||||
_ => new TextBlock { Text = $"No view for {data.GetType().Name}" }
|
_ => 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,
|
/// callback hand-off: when AwaitingCallback never resolves,
|
||||||
/// the OS never re-launched PostIt with the postit:// URL.
|
/// the OS never re-launched PostIt with the postit:// URL.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private OidcLoginPhase _phase = OidcLoginPhase.Idle;
|
private OIDCLoginPhase _phase = OIDCLoginPhase.Idle;
|
||||||
public OidcLoginPhase Phase
|
public OIDCLoginPhase Phase
|
||||||
{
|
{
|
||||||
get => _phase;
|
get => _phase;
|
||||||
private set
|
private set
|
||||||
|
|
@ -122,13 +122,13 @@ public partial class LoginPageViewModel : ViewModelBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string PhaseLabel => _phase switch
|
public string PhaseLabel => _phase switch
|
||||||
{
|
{
|
||||||
OidcLoginPhase.Idle => "En attente",
|
OIDCLoginPhase.Idle => "En attente",
|
||||||
OidcLoginPhase.Discovering => "Découverte OIDC…",
|
OIDCLoginPhase.Discovering => "Découverte OIDC…",
|
||||||
OidcLoginPhase.OpeningBrowser => "Ouverture du navigateur…",
|
OIDCLoginPhase.OpeningBrowser => "Ouverture du navigateur…",
|
||||||
OidcLoginPhase.AwaitingCallback => "En attente du callback postit://…",
|
OIDCLoginPhase.AwaitingCallback => "En attente du callback postit://…",
|
||||||
OidcLoginPhase.ExchangingCode => "Échange du code contre les jetons…",
|
OIDCLoginPhase.ExchangingCode => "Échange du code contre les jetons…",
|
||||||
OidcLoginPhase.Success => "Connecté",
|
OIDCLoginPhase.Success => "Connecté",
|
||||||
OidcLoginPhase.Error => "Erreur",
|
OIDCLoginPhase.Error => "Erreur",
|
||||||
_ => _phase.ToString(),
|
_ => _phase.ToString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -275,7 +275,7 @@ public partial class LoginPageViewModel : ViewModelBase
|
||||||
// The progress sink drives Phase / PhaseLabel; StatusMessage
|
// The progress sink drives Phase / PhaseLabel; StatusMessage
|
||||||
// keeps the text detail (URLs, error messages). Same
|
// keeps the text detail (URLs, error messages). Same
|
||||||
// underlying flow, two views.
|
// underlying flow, two views.
|
||||||
var progress = new Progress<OidcLoginPhase>(p => Phase = p);
|
var progress = new Progress<OIDCLoginPhase>(p => Phase = p);
|
||||||
await LoginInteractiveCoreAsync(_api, progress);
|
await LoginInteractiveCoreAsync(_api, progress);
|
||||||
|
|
||||||
IsBusy = false;
|
IsBusy = false;
|
||||||
|
|
@ -299,7 +299,7 @@ public partial class LoginPageViewModel : ViewModelBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task LoginInteractiveCoreAsync(
|
private async Task LoginInteractiveCoreAsync(
|
||||||
YavscApiClient api,
|
YavscApiClient api,
|
||||||
IProgress<OidcLoginPhase>? progress = null)
|
IProgress<OIDCLoginPhase>? progress = null)
|
||||||
{
|
{
|
||||||
var original = Platform.CreateBrowser;
|
var original = Platform.CreateBrowser;
|
||||||
try
|
try
|
||||||
|
|
|
||||||
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 New}" Content="New post" />
|
||||||
<Button Command="{Binding Save}" Content="Save" />
|
<Button Command="{Binding Save}" Content="Save" />
|
||||||
<Button Command="{Binding Delete}" Content="Delete" />
|
<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>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using PostIt.ViewModels;
|
||||||
|
|
||||||
namespace PostIt.Views;
|
namespace PostIt.Views;
|
||||||
|
|
||||||
|
|
@ -10,4 +13,30 @@ public partial class MainPage : ContentPage
|
||||||
InitializeComponent();
|
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 JustCreatedBy = "just created by ";
|
||||||
public const string LabYouNotOp = "you're no op.";
|
public const string LabYouNotOp = "you're no op.";
|
||||||
public const string LabNoSuchUser = "No such user";
|
public const string LabNoSuchUser = "No such user";
|
||||||
public const string LabNoSuchChan = "No such chan";
|
public const string LabNoSuchChan = "No such chan";
|
||||||
public const string HopWontKickOp = "Half operator cannot kick any operator";
|
public const string HopWontKickOp = "Half operator cannot kick any operator";
|
||||||
public const string LabAuthChatUser = "Authenticated chat user";
|
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 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>
|
/// <summary>
|
||||||
/// Renvoye la facture associée à une clé de facturation,
|
/// Renvoye la facture associée à une clé de facturation,
|
||||||
/// à partir du couple suivant :
|
/// à partir du couple suivant :
|
||||||
///
|
///
|
||||||
/// * un code de facturation
|
/// * un code de facturation
|
||||||
/// (identifiant associé à un type de demande du client)
|
/// (identifiant associé à un type de demande du client)
|
||||||
/// * un entier long identifiant la demande du client
|
/// * un entier long identifiant la demande du client
|
||||||
/// (à une demande, on associe au maximum une seule facture)
|
/// (à 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="billingCode">Identifiant du type de facturation</param>
|
||||||
/// <param name="queryId">Identifiant de la demande du client</param>
|
/// <param name="queryId">Identifiant de la demande du client</param>
|
||||||
/// <returns>La facture</returns>
|
/// <returns>La facture</returns>
|
||||||
Task<IDecidableQuery> GetBillAsync(string billingCode, long queryId);
|
Task<IQuery> GetBillAsync(string billingCode, long queryId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Perfomer settings for the specified performer in the activity
|
/// Perfomer settings for the specified performer in the activity
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="activityCode">activityCode</param>
|
/// <param name="activityCode">activityCode</param>
|
||||||
/// <param name="userId">performer uid</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
|
public interface IDecidableQuery: ITrackedEntity, IQuery
|
||||||
{
|
{
|
||||||
bool Decided { get; set; }
|
|
||||||
bool Accepted { get; set; }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,9 @@ namespace Yavsc
|
||||||
public enum QueryStatus: int
|
public enum QueryStatus: int
|
||||||
{
|
{
|
||||||
Inserted,
|
Inserted,
|
||||||
OwnerValidated,
|
|
||||||
Visited,
|
|
||||||
Rejected,
|
Rejected,
|
||||||
Accepted,
|
Accepted,
|
||||||
InProgress,
|
InProgress,
|
||||||
|
|
||||||
// final states
|
// final states
|
||||||
Failed,
|
Failed,
|
||||||
Success
|
Success
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ using Newtonsoft.Json;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Yavsc.Helpers;
|
using Yavsc.Helpers;
|
||||||
using Yavsc.ViewModels;
|
using Yavsc.ViewModels;
|
||||||
|
using Yavsc.Models.Billing;
|
||||||
|
using Yavsc.Server.Models.FileSystem;
|
||||||
|
|
||||||
namespace Yavsc.ApiControllers
|
namespace Yavsc.ApiControllers
|
||||||
{
|
{
|
||||||
|
|
@ -181,5 +183,203 @@ namespace Yavsc.ApiControllers
|
||||||
if (!fi.Exists) return NotFound(new { Error = "Professional signature not found" });
|
if (!fi.Exists) return NotFound(new { Error = "Professional signature not found" });
|
||||||
return File(fi.OpenRead(), "application/x-pdf", filename); ;
|
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 uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
var now = DateTime.Now;
|
var now = DateTime.Now;
|
||||||
|
|
||||||
var result = _context.RdvQueries.Include(c => c.Location).
|
var result = _context.RdvQueries.Include(c => c.Location).
|
||||||
Include(c => c.Client).Where(c => c.PerformerId == uid && c.Id < maxId && c.EventDate > now
|
Include(c => c.Client).Where(c => c.PerformerId == uid && c.Id < maxId && c.EventDate > now
|
||||||
&& c.ValidationDate == null).
|
&& c.ValidationDate == null).
|
||||||
|
|
@ -49,12 +49,12 @@ namespace Yavsc.Controllers
|
||||||
{
|
{
|
||||||
Client = new ClientProviderInfo {
|
Client = new ClientProviderInfo {
|
||||||
UserName = c.Client.UserName,
|
UserName = c.Client.UserName,
|
||||||
UserId = c.ClientId,
|
UserId = c.ClientId,
|
||||||
Avatar = c.Client.Avatar },
|
Avatar = c.Client.Avatar },
|
||||||
Location = c.Location,
|
Location = c.Location,
|
||||||
EventDate = c.EventDate,
|
EventDate = c.EventDate,
|
||||||
Id = c.Id,
|
Id = c.Id,
|
||||||
Previsional = c.Previsional,
|
Previsional = c.Provisional,
|
||||||
Reason = c.Reason,
|
Reason = c.Reason,
|
||||||
ActivityCode = c.ActivityCode,
|
ActivityCode = c.ActivityCode,
|
||||||
BillingCode = BillingCodes.Rdv
|
BillingCode = BillingCodes.Rdv
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,8 @@ namespace Yavsc.ApiControllers
|
||||||
if (queryId == 0) return BadRequest("queryId");
|
if (queryId == 0) return BadRequest("queryId");
|
||||||
var billing = BillingService.GetBillable(dbContext, billingCode, queryId);
|
var billing = BillingService.GetBillable(dbContext, billingCode, queryId);
|
||||||
if (billing == null) return BadRequest();
|
if (billing == null) return BadRequest();
|
||||||
billing.Decided = true;
|
|
||||||
billing.Accepted = false;
|
billing.Status = QueryStatus.Rejected;
|
||||||
dbContext.SaveChanges();
|
dbContext.SaveChanges();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
@ -47,8 +47,7 @@ namespace Yavsc.ApiControllers
|
||||||
if (queryId == 0) return BadRequest("queryId");
|
if (queryId == 0) return BadRequest("queryId");
|
||||||
var billing = BillingService.GetBillable(dbContext, billingCode, queryId);
|
var billing = BillingService.GetBillable(dbContext, billingCode, queryId);
|
||||||
if (billing == null) return BadRequest();
|
if (billing == null) return BadRequest();
|
||||||
billing.Accepted = true;
|
billing.Status = QueryStatus.Accepted;
|
||||||
billing.Decided = true;
|
|
||||||
dbContext.SaveChanges();
|
dbContext.SaveChanges();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ namespace Yavsc.ApiControllers
|
||||||
// user, as a client
|
// user, as a client
|
||||||
public IActionResult Index()
|
public IActionResult Index()
|
||||||
{
|
{
|
||||||
|
|
||||||
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
|
||||||
var now = DateTime.Now;
|
var now = DateTime.Now;
|
||||||
|
|
@ -151,7 +151,7 @@ namespace Yavsc.ApiControllers
|
||||||
{
|
{
|
||||||
|
|
||||||
HairCutQuery query = await _context.HairCutQueries.Include(q => q.Client).
|
HairCutQuery query = await _context.HairCutQueries.Include(q => q.Client).
|
||||||
Include(q => q.Client.PostalAddress).Include(q => q.Prestation).Include(q=>q.Regularisation)
|
Include(q => q.Client.PostalAddress).Include(q => q.Prestation).Include(q=>q.Regularization)
|
||||||
.SingleAsync(q => q.Id == id);
|
.SingleAsync(q => q.Id == id);
|
||||||
if (query.PaymentId!=null)
|
if (query.PaymentId!=null)
|
||||||
return new BadRequestObjectResult(new { error = "An existing payment process already exists" });
|
return new BadRequestObjectResult(new { error = "An existing payment process already exists" });
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@ using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Yavsc.Attributes.Validation;
|
using Yavsc.Attributes.Validation;
|
||||||
using Yavsc.Helpers;
|
|
||||||
using Yavsc.Models;
|
using Yavsc.Models;
|
||||||
using Yavsc.Models.Messaging;
|
using Yavsc.Models.Messaging;
|
||||||
using Yavsc.Services;
|
using Yavsc.Services;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Yavsc.Server.Helpers;
|
using Yavsc.Server.Helpers;
|
||||||
using static Yavsc.Blogs.Constants;
|
using static Yavsc.Blogs.Constants;
|
||||||
|
using Yavsc.Server.Hubs;
|
||||||
|
|
||||||
namespace Yavsc.Blogs.Controllers
|
namespace Yavsc.Blogs.Controllers
|
||||||
{
|
{
|
||||||
|
|
@ -46,7 +46,7 @@ namespace Yavsc.Blogs.Controllers
|
||||||
}
|
}
|
||||||
logger.LogInformation("validated: api/stream/Put: "+filename);
|
logger.LogInformation("validated: api/stream/Put: "+filename);
|
||||||
var userName = User.GetUserName();
|
var userName = User.GetUserName();
|
||||||
|
|
||||||
string url = string.Format(
|
string url = string.Format(
|
||||||
"{0}/{1}/{2}",
|
"{0}/{1}/{2}",
|
||||||
Config.UserFilesOptions.RequestPath.ToUriComponent(),
|
Config.UserFilesOptions.RequestPath.ToUriComponent(),
|
||||||
|
|
@ -54,7 +54,7 @@ namespace Yavsc.Blogs.Controllers
|
||||||
filename
|
filename
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
string destDir = HttpContext.User.EnsureDestinationDirectory(filePath);
|
string destDir = HttpContext.User.EnsureDestinationDirectory(filePath);
|
||||||
logger.LogInformation($"Saving flow to {destDir}");
|
logger.LogInformation($"Saving flow to {destDir}");
|
||||||
var userId = User.GetUserId();
|
var userId = User.GetUserId();
|
||||||
|
|
@ -65,7 +65,7 @@ namespace Yavsc.Blogs.Controllers
|
||||||
sender = userName,
|
sender = userName,
|
||||||
url = url,
|
url = url,
|
||||||
}, $"{userName} is starting a stream!");
|
}, $"{userName} is starting a stream!");
|
||||||
|
|
||||||
await liveProcessor.AcceptStream(HttpContext, user, destDir, shortFileName);
|
await liveProcessor.AcceptStream(HttpContext, user, destDir, shortFileName);
|
||||||
return Ok();
|
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();
|
WorkflowHelpers.ConfigureBillingService();
|
||||||
|
|
||||||
var firstRegistrar = new Func<ApplicationDbContext, long, IDecidableQuery>((db, id) =>
|
var firstRegistrar = new Func<ApplicationDbContext, long, IQuery>((db, id) =>
|
||||||
db.HairCutQueries.Include(q => q.Prestation).Include(q => q.Regularisation).Single(q => q.Id == id));
|
db.HairCutQueries.Include(q => q.Prestation).Include(q => q.Regularization).Single(q => q.Id == id));
|
||||||
|
|
||||||
const string testCode = "Brush";
|
const string testCode = "Brush";
|
||||||
|
|
||||||
Assert.Throws<InvalidOperationException>(() =>
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
WorkflowHelpers.RegisterBilling<HairCutQuery>(testCode, firstRegistrar));
|
WorkflowHelpers.RegisterBilling<HairCutQuery>(testCode, firstRegistrar));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,10 +93,10 @@ namespace Yavsc.Controllers
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
var user = await GetCurrentUserAsync();
|
var user = await GetCurrentUserAsync();
|
||||||
|
|
||||||
long pc = _dbContext.BlogSpot.Count(x => x.AuthorId == user.Id);
|
long pc = _dbContext.BlogSpot.Count(x => x.AuthorId == user.Id);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var model = new IndexViewModel
|
var model = new IndexViewModel
|
||||||
{
|
{
|
||||||
|
|
@ -123,14 +123,14 @@ namespace Yavsc.Controllers
|
||||||
AllowMonthlyEmail = user.AllowMonthlyEmail,
|
AllowMonthlyEmail = user.AllowMonthlyEmail,
|
||||||
Address = user.PostalAddress?.Address
|
Address = user.PostalAddress?.Address
|
||||||
};
|
};
|
||||||
|
|
||||||
model.HaveProfessionalSettings = _dbContext.Performers.Any(x => x.PerformerId == user.Id);
|
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();
|
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
|
// TODO remember me who this magical a.Settings is built
|
||||||
var usrActToSet = usrActs.Where( a => ( a.Settings == null && a.Does.SettingsClassName != null )).ToArray();
|
var usrActToSet = usrActs.Where( a => ( a.Settings == null && a.Does.SettingsClassName != null )).ToArray();
|
||||||
model.HaveActivityToConfigure = usrActToSet .Count()>0;
|
model.HaveActivityToConfigure = usrActToSet .Count()>0;
|
||||||
model.Activity = _dbContext.UserActivities.Include(a=>a.Does).Where(u=>u.UserId == user.Id).ToList();
|
model.Activity = _dbContext.UserActivities.Include(a=>a.Does).Where(u=>u.UserId == user.Id).ToList();
|
||||||
|
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,7 +152,7 @@ namespace Yavsc.Controllers
|
||||||
var user = await GetCurrentUserAsync();
|
var user = await GetCurrentUserAsync();
|
||||||
user.AllowMonthlyEmail = model.Allow;
|
user.AllowMonthlyEmail = model.Allow;
|
||||||
await this._dbContext.SaveChangesAsync(User.GetUserId());
|
await this._dbContext.SaveChangesAsync(User.GetUserId());
|
||||||
|
|
||||||
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.SetMonthlyEmailSuccess });
|
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.SetMonthlyEmailSuccess });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -302,8 +302,8 @@ namespace Yavsc.Controllers
|
||||||
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
|
||||||
var calendars = await _calendarManager.GetCalendarsAsync(pageToken);
|
var calendars = await _calendarManager.GetCalendarsAsync(pageToken);
|
||||||
return View(new SetGoogleCalendarViewModel {
|
return View(new SetGoogleCalendarViewModel {
|
||||||
ReturnUrl = returnUrl,
|
ReturnUrl = returnUrl,
|
||||||
Calendars = calendars
|
Calendars = calendars
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -343,9 +343,9 @@ namespace Yavsc.Controllers
|
||||||
)) return BadRequest(new { message = "data already present" });
|
)) return BadRequest(new { message = "data already present" });
|
||||||
|
|
||||||
user.BankInfo.Add(model);
|
user.BankInfo.Add(model);
|
||||||
|
|
||||||
_dbContext.Update(user);
|
_dbContext.Update(user);
|
||||||
|
|
||||||
await _dbContext.SaveChangesAsync();
|
await _dbContext.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.SetBankInfoSuccess });
|
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.SetBankInfoSuccess });
|
||||||
|
|
@ -495,7 +495,7 @@ namespace Yavsc.Controllers
|
||||||
return View("Error");
|
return View("Error");
|
||||||
}
|
}
|
||||||
var userLogins = await _userManager.GetLoginsAsync(user);
|
var userLogins = await _userManager.GetLoginsAsync(user);
|
||||||
|
|
||||||
ViewBag.ShowRemoveButton = user.PasswordHash != null || userLogins.Count > 1;
|
ViewBag.ShowRemoveButton = user.PasswordHash != null || userLogins.Count > 1;
|
||||||
|
|
||||||
return View(new ManageLoginsViewModel
|
return View(new ManageLoginsViewModel
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,24 @@
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Yavsc.Helpers;
|
using Microsoft.Extensions.Localization;
|
||||||
using Yavsc.Models;
|
using Yavsc.Models;
|
||||||
using Yavsc.Models.Workflow;
|
using Yavsc.Models.Workflow;
|
||||||
using Yavsc.Server.Helpers;
|
using Yavsc.Server.Helpers;
|
||||||
|
using Yavsc.Services;
|
||||||
|
|
||||||
namespace Yavsc.Controllers
|
namespace Yavsc.Controllers
|
||||||
{
|
{
|
||||||
public class CommandFormsController : Controller
|
public class CommandFormsController : Controller
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
|
private readonly IStringLocalizer<CommandFormsController> _localizer;
|
||||||
|
|
||||||
public CommandFormsController(ApplicationDbContext context)
|
public CommandFormsController(ApplicationDbContext context,
|
||||||
|
IStringLocalizer<CommandFormsController> localizer)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_localizer = localizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: CommandForms
|
// GET: CommandForms
|
||||||
|
|
@ -47,11 +51,14 @@ namespace Yavsc.Controllers
|
||||||
SetViewBag();
|
SetViewBag();
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetViewBag(CommandForm commandForm = null)
|
private void SetViewBag(CommandForm commandForm = null)
|
||||||
{
|
{
|
||||||
ViewBag.ActivityCode = new SelectList(_context.Activities, "Code", "Name", commandForm?.ActivityCode);
|
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
|
// POST: CommandForms/Create
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
|
|
|
||||||
|
|
@ -36,16 +36,16 @@ namespace Yavsc.Controllers
|
||||||
public ActionResult Index()
|
public ActionResult Index()
|
||||||
{
|
{
|
||||||
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
var now = DateTime.Now;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
var model = new FrontOfficeIndexViewModel
|
var model = new FrontOfficeIndexViewModel
|
||||||
{
|
{
|
||||||
EstimateToProduceCount = _context.RdvQueries.Where(c => c.PerformerId == uid && c.EventDate > now
|
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 && e.ProviderValidationDate != null))).Count(),
|
&& c.ValidationDate == null && !_context.Estimates.Any(e => e.CommandId == c.Id)).Count(),
|
||||||
EstimateToSignAsProCount = _context.RdvQueries.Where(c => (c.PerformerId == uid && c.EventDate > now
|
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 && e.ProviderValidationDate != null)))).Count(),
|
&& c.ValidationDate == null && _context.Estimates.Any(e => e.CommandId == c.Id )).Count(),
|
||||||
EstimateToSignAsCliCount = _context.Estimates.Where(e => e.ClientId == uid && e.ClientValidationDate == null).Count(),
|
EstimateToSignAsCliCount = _context.Estimates.Where(e => e.ClientId == uid && e.Query.Status == QueryStatus.Accepted).Count(),
|
||||||
BillToSignAsProCount = 0,
|
|
||||||
BillToSignAsCliCount = 0,
|
BillToSignAsCliCount = 0,
|
||||||
NewPayementsCount = 0
|
NewPayementsCount = 0
|
||||||
};
|
};
|
||||||
|
|
@ -65,14 +65,14 @@ namespace Yavsc.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
[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");
|
throw new NotImplementedException("No Activity code");
|
||||||
}
|
}
|
||||||
ViewBag.Activity = await _context.Activities.FirstOrDefaultAsync(a => a.Code == id);
|
ViewBag.Activity = await _context.Activities.FirstOrDefaultAsync(a => a.Code == activityCode);
|
||||||
var result = await _context.ListPerformersAsync(_billing, id);
|
var result = await _context.ListPerformersAsync(_billing, activityCode);
|
||||||
return View(result);
|
return View(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ namespace Yavsc.Controllers
|
||||||
// GET: GeneralSettings
|
// GET: GeneralSettings
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
return View(await _context.GeneralSettings.ToListAsync());
|
return View(await _context.MusicLoverSettings.ToListAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: GeneralSettings/Details/5
|
// GET: GeneralSettings/Details/5
|
||||||
|
|
@ -28,7 +28,7 @@ namespace Yavsc.Controllers
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
MusicLoverSettings generalSettings = await _context.GeneralSettings.SingleAsync(m => m.UserId == id);
|
MusicLoverSettings generalSettings = await _context.MusicLoverSettings.SingleAsync(m => m.UserId == id);
|
||||||
if (generalSettings == null)
|
if (generalSettings == null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
@ -50,7 +50,7 @@ namespace Yavsc.Controllers
|
||||||
{
|
{
|
||||||
if (ModelState.IsValid)
|
if (ModelState.IsValid)
|
||||||
{
|
{
|
||||||
_context.GeneralSettings.Add(generalSettings);
|
_context.MusicLoverSettings.Add(generalSettings);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +65,7 @@ namespace Yavsc.Controllers
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
MusicLoverSettings generalSettings = await _context.GeneralSettings.SingleAsync(m => m.UserId == id);
|
MusicLoverSettings generalSettings = await _context.MusicLoverSettings.SingleAsync(m => m.UserId == id);
|
||||||
if (generalSettings == null)
|
if (generalSettings == null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
@ -96,7 +96,7 @@ namespace Yavsc.Controllers
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
MusicLoverSettings generalSettings = await _context.GeneralSettings.SingleAsync(m => m.UserId == id);
|
MusicLoverSettings generalSettings = await _context.MusicLoverSettings.SingleAsync(m => m.UserId == id);
|
||||||
if (generalSettings == null)
|
if (generalSettings == null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
@ -110,8 +110,8 @@ namespace Yavsc.Controllers
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> DeleteConfirmed(string id)
|
public async Task<IActionResult> DeleteConfirmed(string id)
|
||||||
{
|
{
|
||||||
MusicLoverSettings generalSettings = await _context.GeneralSettings.SingleAsync(m => m.UserId == id);
|
MusicLoverSettings generalSettings = await _context.MusicLoverSettings.SingleAsync(m => m.UserId == id);
|
||||||
_context.GeneralSettings.Remove(generalSettings);
|
_context.MusicLoverSettings.Remove(generalSettings);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ namespace Yavsc.Controllers.Generic
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Models;
|
using Models;
|
||||||
using Yavsc.Helpers;
|
|
||||||
using Yavsc.Server.Helpers;
|
using Yavsc.Server.Helpers;
|
||||||
using Yavsc.Services;
|
using Yavsc.Services;
|
||||||
|
|
||||||
|
|
@ -37,7 +36,7 @@ namespace Yavsc.Controllers.Generic
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
return View(await GetSettingsAsync(User.GetUserId()));
|
return View(await GetSettingsAsync(User.GetUserId()));
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ namespace Yavsc.Controllers
|
||||||
this.haircutLocalizer = haircutLocalizer;
|
this.haircutLocalizer = haircutLocalizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<HairCutQuery> GetQuery(long id)
|
private async Task<HairCutQuery> GetQuery(long id)
|
||||||
{
|
{
|
||||||
var query = await _context.HairCutQueries
|
var query = await _context.HairCutQueries
|
||||||
|
|
@ -58,7 +58,7 @@ namespace Yavsc.Controllers
|
||||||
.Include(x => x.Prestation)
|
.Include(x => x.Prestation)
|
||||||
.Include(x => x.PerformerProfile.Performer)
|
.Include(x => x.PerformerProfile.Performer)
|
||||||
.Include(x => x.PerformerProfile.Performer.DeviceDeclaration)
|
.Include(x => x.PerformerProfile.Performer.DeviceDeclaration)
|
||||||
.Include(x => x.Regularisation)
|
.Include(x => x.Regularization)
|
||||||
.SingleAsync(m => m.Id == id);
|
.SingleAsync(m => m.Id == id);
|
||||||
query.SelectedProfile = await _context.BrusherProfile.SingleAsync(b => b.UserId == query.PerformerId);
|
query.SelectedProfile = await _context.BrusherProfile.SingleAsync(b => b.UserId == query.PerformerId);
|
||||||
return query;
|
return query;
|
||||||
|
|
@ -82,11 +82,11 @@ namespace Yavsc.Controllers
|
||||||
}
|
}
|
||||||
var paymentInfo = await _context.ConfirmPayment(User.GetUserId(), PayerID, token);
|
var paymentInfo = await _context.ConfirmPayment(User.GetUserId(), PayerID, token);
|
||||||
ViewBag.paymentinfo = paymentInfo;
|
ViewBag.paymentinfo = paymentInfo;
|
||||||
command.Regularisation = paymentInfo.DbContent;
|
command.Regularization = paymentInfo.DbContent;
|
||||||
command.PaymentId = token;
|
command.PaymentId = token;
|
||||||
bool paymentOk = false;
|
bool paymentOk = false;
|
||||||
if (paymentInfo.DetailsFromPayPal != null)
|
if (paymentInfo.DetailsFromPayPal != null)
|
||||||
if (paymentInfo.DetailsFromPayPal.Ack == AckCodeType.SUCCESS)
|
if (paymentInfo.DetailsFromPayPal.Ack == AckCodeType.SUCCESS)
|
||||||
{
|
{
|
||||||
// FIXME Assert (command.ValidationDate == null)
|
// FIXME Assert (command.ValidationDate == null)
|
||||||
if (command.ValidationDate == null) {
|
if (command.ValidationDate == null) {
|
||||||
|
|
@ -174,7 +174,7 @@ namespace Yavsc.Controllers
|
||||||
.Include(x => x.PerformerProfile)
|
.Include(x => x.PerformerProfile)
|
||||||
.Include(x => x.Prestation)
|
.Include(x => x.Prestation)
|
||||||
.Include(x => x.PerformerProfile.Performer)
|
.Include(x => x.PerformerProfile.Performer)
|
||||||
.Include(x => x.Regularisation)
|
.Include(x => x.Regularization)
|
||||||
.SingleOrDefaultAsync(m => m.Id == id);
|
.SingleOrDefaultAsync(m => m.Id == id);
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
|
|
@ -224,7 +224,7 @@ namespace Yavsc.Controllers
|
||||||
.FirstOrDefault(
|
.FirstOrDefault(
|
||||||
x => x.PerformerId == model.PerformerId
|
x => x.PerformerId == model.PerformerId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
if (taintIds != null)
|
if (taintIds != null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ namespace Yavsc.Controllers
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
readonly IStringLocalizer<ProjectController> _localizer;
|
readonly IStringLocalizer<ProjectController> _localizer;
|
||||||
readonly IStringLocalizer<BugController> _bugLocalizer;
|
readonly IStringLocalizer<BugController> _bugLocalizer;
|
||||||
|
|
||||||
public ProjectController(ApplicationDbContext context,
|
public ProjectController(ApplicationDbContext context,
|
||||||
IStringLocalizer<ProjectController> localizer,
|
IStringLocalizer<ProjectController> localizer,
|
||||||
IStringLocalizer<BugController> bugLocalizer
|
IStringLocalizer<BugController> bugLocalizer
|
||||||
|
|
@ -32,7 +32,7 @@ namespace Yavsc.Controllers
|
||||||
// GET: Project
|
// GET: Project
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
var applicationDbContext = _context.Project.Include(p => p.Client).Include(p => p.Context).Include(p => p.PerformerProfile).Include(p => p.Regularisation).Include(p => p.Repository);
|
var applicationDbContext = _context.Project.Include(p => p.Client).Include(p => p.Context).Include(p => p.PerformerProfile).Include(p => p.Regularization).Include(p => p.Repository);
|
||||||
return View(await applicationDbContext.ToListAsync());
|
return View(await applicationDbContext.ToListAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@
|
||||||
|
|
||||||
<!-- Yavsc.Org-specific versions -->
|
<!-- Yavsc.Org-specific versions -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="AsciiDocSharp" Version="0.2.0" />
|
<PackageVersion Include="AsciiDocSharp" Version="0.1.0" />
|
||||||
<PackageVersion Include="AsciiDocSharp.Converters.Html" Version="0.2.0" />
|
<PackageVersion Include="AsciiDocSharp.Converters.Html" Version="0.1.0" />
|
||||||
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.6.2" />
|
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.6.2" />
|
||||||
<PackageVersion Include="Google.Apis.Compute.v1" Version="1.74.0.4138" />
|
<PackageVersion Include="Google.Apis.Compute.v1" Version="1.74.0.4138" />
|
||||||
<PackageVersion Include="HigginsSoft.IdentityServer8.AspNetIdentity" Version="8.0.5-preview-net9" />
|
<PackageVersion Include="HigginsSoft.IdentityServer8.AspNetIdentity" Version="8.1.0-alpha.171" />
|
||||||
<PackageVersion Include="HigginsSoft.IdentityServer8.EntityFramework.Storage" Version="8.0.5-preview-net9" />
|
<PackageVersion Include="HigginsSoft.IdentityServer8.EntityFramework.Storage" Version="8.1.0-alpha.171" />
|
||||||
<PackageVersion Include="HigginsSoft.IdentityServer8.Security" Version="8.0.5-preview-net9" />
|
<PackageVersion Include="HigginsSoft.IdentityServer8.Security" Version="8.1.0-alpha.171" />
|
||||||
<PackageVersion Include="HigginsSoft.IdentityServer8.Storage" Version="8.0.5-preview-net9" />
|
<PackageVersion Include="HigginsSoft.IdentityServer8.Storage" Version="8.1.0-alpha.171" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Antiforgery" Version="2.3.11" />
|
<PackageVersion Include="Microsoft.AspNetCore.Antiforgery" Version="2.3.11" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.Google" Version="10.0.9" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authentication.Google" Version="10.0.9" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="10.0.9" />
|
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="10.0.9" />
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
|
|
@ -23,7 +20,7 @@ namespace Yavsc.Extensions
|
||||||
var typeInfo = type.GetTypeInfo();
|
var typeInfo = type.GetTypeInfo();
|
||||||
var values = Enum.GetValues(type).Cast<Enum>();
|
var values = Enum.GetValues(type).Cast<Enum>();
|
||||||
var items = new List<SelectListItem>();
|
var items = new List<SelectListItem>();
|
||||||
|
|
||||||
foreach (var value in values)
|
foreach (var value in values)
|
||||||
{
|
{
|
||||||
items.Add(new SelectListItem {
|
items.Add(new SelectListItem {
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ using Yavsc.Settings;
|
||||||
using Yavsc.ViewModels.Auth;
|
using Yavsc.ViewModels.Auth;
|
||||||
using IdentityServer8.Models;
|
using IdentityServer8.Models;
|
||||||
using IdentityServer8.EntityFramework.Mappers;
|
using IdentityServer8.EntityFramework.Mappers;
|
||||||
|
using Yavsc.Server.Hubs;
|
||||||
|
|
||||||
namespace Yavsc.Extensions;
|
namespace Yavsc.Extensions;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@ namespace Yavsc.Helpers
|
||||||
{
|
{
|
||||||
Sender = query.ClientId,
|
Sender = query.ClientId,
|
||||||
Reason = query.Reason,
|
Reason = query.Reason,
|
||||||
Client = new ClientProviderInfo {
|
Client = new ClientProviderInfo {
|
||||||
UserName = query.Client.UserName ,
|
UserName = query.Client.UserName ,
|
||||||
UserId = query.ClientId,
|
UserId = query.ClientId,
|
||||||
Avatar = query.Client.Avatar } ,
|
Avatar = query.Client.Avatar } ,
|
||||||
Previsional = query.Previsional,
|
Previsional = query.Provisional,
|
||||||
EventDate = query.EventDate,
|
EventDate = query.EventDate,
|
||||||
Location = query.Location,
|
Location = query.Location,
|
||||||
Id = query.Id,
|
Id = query.Id,
|
||||||
|
|
@ -44,7 +44,7 @@ namespace Yavsc.Helpers
|
||||||
var yaev = query.CreateEvent("NewHairCutQuery",
|
var yaev = query.CreateEvent("NewHairCutQuery",
|
||||||
string.Format(SR["HairCutQueryValidation"],query.Client.UserName),
|
string.Format(SR["HairCutQueryValidation"],query.Client.UserName),
|
||||||
$"{query.Client.Id}");
|
$"{query.Client.Id}");
|
||||||
|
|
||||||
|
|
||||||
return yaev;
|
return yaev;
|
||||||
}
|
}
|
||||||
|
|
@ -58,12 +58,12 @@ namespace Yavsc.Helpers
|
||||||
var yaev = new HairCutQueryEvent("newCommand")
|
var yaev = new HairCutQueryEvent("newCommand")
|
||||||
{
|
{
|
||||||
Sender = query.ClientId,
|
Sender = query.ClientId,
|
||||||
|
|
||||||
Client = new ClientProviderInfo {
|
Client = new ClientProviderInfo {
|
||||||
UserName = query.Client.UserName ,
|
UserName = query.Client.UserName ,
|
||||||
UserId = query.ClientId,
|
UserId = query.ClientId,
|
||||||
Avatar = query.Client.Avatar } ,
|
Avatar = query.Client.Avatar } ,
|
||||||
Previsional = query.Previsional,
|
Previsional = query.Provisional,
|
||||||
EventDate = query.EventDate,
|
EventDate = query.EventDate,
|
||||||
Location = query.Location,
|
Location = query.Location,
|
||||||
Id = query.Id,
|
Id = query.Id,
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,16 @@ namespace Yavsc.Helpers {
|
||||||
this ApplicationDbContext _dbContext, List<UserActivity> activity)
|
this ApplicationDbContext _dbContext, List<UserActivity> activity)
|
||||||
{
|
{
|
||||||
var activities = activity.ToArray();
|
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() {
|
x=> new SelectListItem() {
|
||||||
Value = x.Code, Text = x.Name, Selected = activities.Any(a=>a.DoesCode == x.Code)
|
Value = x.Code, Text = x.Name, Selected = activities.Any(a=>a.DoesCode == x.Code)
|
||||||
} ).ToList();
|
} ).ToList();
|
||||||
|
|
||||||
return items;
|
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
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "10.0.8")
|
.HasAnnotation("ProductVersion", "10.0.9")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
@ -1413,6 +1413,50 @@ namespace Yavsc.Migrations
|
||||||
b.ToTable("ExceptionsSIREN");
|
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 =>
|
modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("FileId")
|
b.Property<long>("FileId")
|
||||||
|
|
@ -2157,7 +2201,8 @@ namespace Yavsc.Migrations
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
.HasColumnType("text");
|
.HasMaxLength(10240)
|
||||||
|
.HasColumnType("character varying(10240)");
|
||||||
|
|
||||||
b.Property<long?>("FeatureId")
|
b.Property<long?>("FeatureId")
|
||||||
.HasColumnType("bigint");
|
.HasColumnType("bigint");
|
||||||
|
|
@ -2534,7 +2579,7 @@ namespace Yavsc.Migrations
|
||||||
b.Property<string>("DjSettingsUserId")
|
b.Property<string>("DjSettingsUserId")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("GeneralSettingsUserId")
|
b.Property<string>("MusicLoverSettingsUserId")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<int>("Rate")
|
b.Property<int>("Rate")
|
||||||
|
|
@ -2547,7 +2592,7 @@ namespace Yavsc.Migrations
|
||||||
|
|
||||||
b.HasIndex("DjSettingsUserId");
|
b.HasIndex("DjSettingsUserId");
|
||||||
|
|
||||||
b.HasIndex("GeneralSettingsUserId");
|
b.HasIndex("MusicLoverSettingsUserId");
|
||||||
|
|
||||||
b.HasIndex("TendencyId");
|
b.HasIndex("TendencyId");
|
||||||
|
|
||||||
|
|
@ -2585,16 +2630,6 @@ namespace Yavsc.Migrations
|
||||||
b.ToTable("DjSettings");
|
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 =>
|
modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("InstrumentId")
|
b.Property<long>("InstrumentId")
|
||||||
|
|
@ -2610,6 +2645,16 @@ namespace Yavsc.Migrations
|
||||||
b.ToTable("Instrumentation");
|
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 =>
|
modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("CreationToken")
|
b.Property<string>("CreationToken")
|
||||||
|
|
@ -2894,6 +2939,9 @@ namespace Yavsc.Migrations
|
||||||
b.Property<bool>("Hidden")
|
b.Property<bool>("Hidden")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("Moderated")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("ModeratorGroupName")
|
b.Property<string>("ModeratorGroupName")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
|
@ -3737,6 +3785,25 @@ namespace Yavsc.Migrations
|
||||||
b.Navigation("Query");
|
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 =>
|
modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Yavsc.Models.Blog.UploadedFile", "File")
|
b.HasOne("Yavsc.Models.Blog.UploadedFile", "File")
|
||||||
|
|
@ -4162,9 +4229,9 @@ namespace Yavsc.Migrations
|
||||||
.WithMany("SoundColor")
|
.WithMany("SoundColor")
|
||||||
.HasForeignKey("DjSettingsUserId");
|
.HasForeignKey("DjSettingsUserId");
|
||||||
|
|
||||||
b.HasOne("Yavsc.Models.Musical.Profiles.GeneralSettings", null)
|
b.HasOne("Yavsc.Models.Musical.Profiles.MusicLoverSettings", null)
|
||||||
.WithMany("SoundColor")
|
.WithMany("SoundColor")
|
||||||
.HasForeignKey("GeneralSettingsUserId");
|
.HasForeignKey("MusicLoverSettingsUserId");
|
||||||
|
|
||||||
b.HasOne("Yavsc.Models.Musical.MusicalTendency", "MusicalTendency")
|
b.HasOne("Yavsc.Models.Musical.MusicalTendency", "MusicalTendency")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
|
|
@ -4519,6 +4586,8 @@ namespace Yavsc.Migrations
|
||||||
modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b =>
|
modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Bill");
|
b.Navigation("Bill");
|
||||||
|
|
||||||
|
b.Navigation("Signatures");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b =>
|
modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b =>
|
||||||
|
|
@ -4580,7 +4649,7 @@ namespace Yavsc.Migrations
|
||||||
b.Navigation("SoundColor");
|
b.Navigation("SoundColor");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Yavsc.Models.Musical.Profiles.GeneralSettings", b =>
|
modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("SoundColor");
|
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)
|
public void OnConnected(string cxId, bool isCop)
|
||||||
{
|
{
|
||||||
var username = ChatUserNames[cxId];
|
var username = ChatUserNames[cxId];
|
||||||
if (!IsConnected(username))
|
if (!IsConnected(username))
|
||||||
ChatRoomPresence[username] = new List<string>();
|
ChatRoomPresence[username] = new List<string>();
|
||||||
_isCop[username] = isCop;
|
_isCop[username] = isCop;
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +101,7 @@ namespace Yavsc.Services
|
||||||
return _isCop[userName];
|
return _isCop[userName];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnDisctonnected(string connectionId)
|
public void OnDisconnected(string connectionId)
|
||||||
{
|
{
|
||||||
string uname;
|
string uname;
|
||||||
|
|
||||||
|
|
@ -136,7 +136,7 @@ namespace Yavsc.Services
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// FIXME only remove cx, not username,
|
// 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
|
// from another device, to the same room
|
||||||
chanInfo.Users.Remove(cxId);
|
chanInfo.Users.Remove(cxId);
|
||||||
if (chanInfo.Users.Count == 0)
|
if (chanInfo.Users.Count == 0)
|
||||||
|
|
@ -183,7 +183,7 @@ namespace Yavsc.Services
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
chanInfo.Users.Add(cxId);
|
chanInfo.Users.Add(cxId);
|
||||||
}
|
}
|
||||||
_logger.LogInformation($"existing room joint: {userName}=>{roomName}");
|
_logger.LogInformation($"existing room joint: {userName}=>{roomName}");
|
||||||
if (!ChatRoomPresence[userName].Contains(roomName))
|
if (!ChatRoomPresence[userName].Contains(roomName))
|
||||||
ChatRoomPresence[userName].Add(roomName);
|
ChatRoomPresence[userName].Add(roomName);
|
||||||
|
|
@ -200,7 +200,7 @@ namespace Yavsc.Services
|
||||||
// room was closed.
|
// room was closed.
|
||||||
var room = _dbContext.ChatRoom.FirstOrDefault(r => r.Name == roomName);
|
var room = _dbContext.ChatRoom.FirstOrDefault(r => r.Name == roomName);
|
||||||
chanInfo = new ChatRoomInfo();
|
chanInfo = new ChatRoomInfo();
|
||||||
|
|
||||||
|
|
||||||
if (room != null)
|
if (room != null)
|
||||||
{
|
{
|
||||||
|
|
@ -244,7 +244,7 @@ namespace Yavsc.Services
|
||||||
throw new System.NotImplementedException();
|
throw new System.NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Dehop(string roomName, string userName)
|
public bool DeHop(string roomName, string userName)
|
||||||
{
|
{
|
||||||
throw new System.NotImplementedException();
|
throw new System.NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
@ -292,12 +292,12 @@ namespace Yavsc.Services
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Channels.TryGetValue(roomName, out chanInfo))
|
if (!Channels.TryGetValue(roomName, out chanInfo))
|
||||||
{
|
{
|
||||||
_errorHandler(roomName, _localizer.GetString(ChatHubConstants.LabNoSuchChan).ToString());
|
_errorHandler(roomName, _localizer.GetString(ChatHubConstants.LabNoSuchChan).ToString());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var kickerName = GetUserName(cxId);
|
var kickerName = GetUserName(cxId);
|
||||||
if (!chanInfo.Ops.Contains(cxId))
|
if (!chanInfo.Ops.Contains(cxId))
|
||||||
if (!chanInfo.Hops.Contains(cxId))
|
if (!chanInfo.Hops.Contains(cxId))
|
||||||
|
|
@ -325,19 +325,19 @@ namespace Yavsc.Services
|
||||||
}
|
}
|
||||||
|
|
||||||
// all good, time to kick :-)
|
// all good, time to kick :-)
|
||||||
foreach (var ucx in ucxs) {
|
foreach (var ucx in ucxs) {
|
||||||
if (chanInfo.Users.Contains(ucx))
|
if (chanInfo.Users.Contains(ucx))
|
||||||
chanInfo.Users.Remove(ucx);
|
chanInfo.Users.Remove(ucx);
|
||||||
|
|
||||||
else if (chanInfo.Ops.Contains(ucx))
|
else if (chanInfo.Ops.Contains(ucx))
|
||||||
chanInfo.Ops.Remove(ucx);
|
chanInfo.Ops.Remove(ucx);
|
||||||
|
|
||||||
else if (chanInfo.Hops.Contains(ucx))
|
else if (chanInfo.Hops.Contains(ucx))
|
||||||
chanInfo.Hops.Remove(ucx);
|
chanInfo.Hops.Remove(ucx);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Yavsc.Interface;
|
using Yavsc.Interface;
|
||||||
|
|
@ -8,6 +7,7 @@ using Yavsc.Models;
|
||||||
using Yavsc.Models.Google.Messaging;
|
using Yavsc.Models.Google.Messaging;
|
||||||
using Yavsc.Models.Haircut;
|
using Yavsc.Models.Haircut;
|
||||||
using Yavsc.Models.Messaging;
|
using Yavsc.Models.Messaging;
|
||||||
|
using Yavsc.Server.Hubs;
|
||||||
|
|
||||||
namespace Yavsc.Services
|
namespace Yavsc.Services
|
||||||
{
|
{
|
||||||
|
|
@ -92,25 +92,25 @@ namespace Yavsc.Services
|
||||||
|
|
||||||
|
|
||||||
_logger.LogDebug($"Sending to {user.UserName} <{user.Email}> : {body}");
|
_logger.LogDebug($"Sending to {user.UserName} <{user.Email}> : {body}");
|
||||||
|
|
||||||
result.message_id = await _emailSender.SendEmailAsync(user.UserName, user.Email,
|
result.message_id = await _emailSender.SendEmailAsync(user.UserName, user.Email,
|
||||||
$"{ev.Sender} (un client) vous demande un rendez-vous",
|
$"{ev.Sender} (un client) vous demande un rendez-vous",
|
||||||
body + Environment.NewLine);
|
body + Environment.NewLine);
|
||||||
response.success++;
|
response.success++;
|
||||||
|
|
||||||
var cxids = _cxManager.GetConnexionIds(user.UserName);
|
var cxIds = _cxManager.GetConnexionIds(user.UserName);
|
||||||
if (cxids == null)
|
if (cxIds == null)
|
||||||
{
|
{
|
||||||
_logger.LogDebug($"no cx to {user.UserName} <{user.Email}> ");
|
_logger.LogDebug($"no cx to {user.UserName} <{user.Email}> ");
|
||||||
}
|
}
|
||||||
else
|
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);
|
// 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>
|
var data = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
["event"] = JsonConvert.SerializeObject(ev)
|
["event"] = JsonConvert.SerializeObject(ev)
|
||||||
|
|
@ -152,22 +152,9 @@ namespace Yavsc.Services
|
||||||
return await NotifyEvent<HairCutQueryEvent>(userIds, ev);
|
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="container">
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
<h1>Access Denied</h1>
|
<h1>Access Denied</h1>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
@model ForgotPasswordViewModel
|
@model ForgotPasswordViewModel
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Account"];
|
||||||
|
}
|
||||||
|
|
||||||
<form asp-controller="Account" asp-action="ForgotPassword">
|
<form asp-controller="Account" asp-action="ForgotPassword">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
@ -6,4 +9,4 @@
|
||||||
<input class="form-control" placeholder="LoginOrEmail" asp-for="LoginOrEmail" autofocus>
|
<input class="form-control" placeholder="LoginOrEmail" asp-for="LoginOrEmail" autofocus>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" name="button" value="send">Send me a recovery link</button>
|
<button class="btn btn-primary" name="button" value="send">Send me a recovery link</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
@model string
|
@model string
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Account"];
|
||||||
|
}
|
||||||
|
|
||||||
<h1>Check your mail box!</h1>
|
<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
|
// set this so the layout rendering sees an anonymous user
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
@model LoginViewModel
|
@model LoginViewModel
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Account"];
|
||||||
|
}
|
||||||
|
|
||||||
<div class="login-page">
|
<div class="login-page">
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
|
|
@ -84,4 +87,4 @@
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
@model LogoutViewModel
|
@{
|
||||||
|
ViewBag.Title = Localizer["Account"];
|
||||||
|
}
|
||||||
|
|
||||||
|
@model LogoutViewModel
|
||||||
|
|
||||||
<div class="logout-page">
|
<div class="logout-page">
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
@model RegisterModel
|
@model RegisterModel
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Account"];
|
||||||
|
}
|
||||||
|
|
||||||
<partial name="_ValidationSummary" />
|
<partial name="_ValidationSummary" />
|
||||||
<form method="post">
|
<form method="post">
|
||||||
@Html.EditorForModel()
|
@Html.EditorForModel()
|
||||||
<button class="btn btn-primary" name="button"
|
<button class="btn btn-primary" name="button"
|
||||||
value="Register">Register</button>
|
value="Register">Register</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
@model ResetPasswordViewModel
|
@model ResetPasswordViewModel
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Account"];
|
||||||
|
}
|
||||||
|
|
||||||
<form asp-route-id="@Model.Id" asp-route-code="@Model.Code">
|
<form asp-route-id="@Model.Id" asp-route-code="@Model.Code">
|
||||||
<p>Your email : <code>@Model.Email</code></p>
|
<p>Your email : <code>@Model.Email</code></p>
|
||||||
|
|
@ -15,4 +18,4 @@
|
||||||
|
|
||||||
<button class="btn btn-primary" name="button" value="Reset">Reset Password</button>
|
<button class="btn btn-primary" name="button" value="Reset">Reset Password</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
@model SignInModel
|
@model SignInModel
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Account"];
|
||||||
|
}
|
||||||
|
|
||||||
<div class="login-page">
|
<div class="login-page">
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
|
|
@ -84,4 +87,4 @@
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
@model HaircutAdminViewModel
|
@model HaircutAdminViewModel
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Administration"];
|
||||||
|
}
|
||||||
|
|
||||||
<a asp-controller="HairTaints" class="btn btn-primary">
|
<a asp-controller="HairTaints" class="btn btn-primary">
|
||||||
Gestion des couleurs
|
Gestion des couleurs
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,19 @@
|
||||||
@foreach (var user in Model.Users) {
|
@foreach (var user in Model.Users) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<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>
|
<span> @user.UserName <@(user.Email)> </span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,9 @@
|
||||||
@model IdentityServer8.EntityFramework.Entities.ApiScope
|
@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>
|
<h4>ApiScope</h4>
|
||||||
<hr />
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<form asp-action="Create">
|
<form asp-action="Create">
|
||||||
|
|
@ -64,6 +53,3 @@
|
||||||
<div>
|
<div>
|
||||||
<a asp-action="Index">Back to List</a>
|
<a asp-action="Index">Back to List</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,8 @@
|
||||||
@model Yavsc.Models.YavscApiScope
|
@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>
|
<h3>Are you sure you want to delete this?</h3>
|
||||||
<div>
|
<div>
|
||||||
<h4>YavscApiScope</h4>
|
<h4>YavscApiScope</h4>
|
||||||
|
|
@ -61,12 +51,9 @@
|
||||||
@Html.DisplayFor(model => model.ShowInDiscoveryDocument)
|
@Html.DisplayFor(model => model.ShowInDiscoveryDocument)
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<form asp-action="Delete">
|
<form asp-action="Delete">
|
||||||
<input type="hidden" asp-for="Id" />
|
<input type="hidden" asp-for="Id" />
|
||||||
<input type="submit" value="Delete" class="btn btn-danger" /> |
|
<input type="submit" value="Delete" class="btn btn-danger" /> |
|
||||||
<a asp-action="Index">Back to List</a>
|
<a asp-action="Index">Back to List</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,8 @@
|
||||||
@model Yavsc.Models.YavscApiScope
|
@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>
|
<div>
|
||||||
<h4>YavscApiScope</h4>
|
<h4>YavscApiScope</h4>
|
||||||
|
|
@ -65,5 +56,3 @@
|
||||||
<a asp-action="Edit" asp-route-id="@Model?.Id">Edit</a> |
|
<a asp-action="Edit" asp-route-id="@Model?.Id">Edit</a> |
|
||||||
<a asp-action="Index">Back to List</a>
|
<a asp-action="Index">Back to List</a>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,8 @@
|
||||||
@model Yavsc.Models.YavscApiScope
|
@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>
|
<h4>YavscApiScope</h4>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
@ -65,6 +56,3 @@
|
||||||
<div>
|
<div>
|
||||||
<a asp-action="Index">Back to List</a>
|
<a asp-action="Index">Back to List</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,10 @@
|
||||||
@model IEnumerable<Yavsc.Models.YavscApiScope>
|
@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>
|
<p>
|
||||||
<a asp-action="Create">Create New</a>
|
<a asp-action="Create">Create New</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -75,5 +68,3 @@
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
@model Client
|
@model Client
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Client"];
|
||||||
|
}
|
||||||
|
|
||||||
<h2>@Localizer["Create"]</h2>
|
<h2>@Localizer["Create"]</h2>
|
||||||
|
|
||||||
|
|
@ -77,4 +80,3 @@
|
||||||
<div>
|
<div>
|
||||||
<a asp-action="Index">@Localizer["Back to List"]</a>
|
<a asp-action="Index">@Localizer["Back to List"]</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
@model Client
|
@model Client
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Client"];
|
||||||
|
}
|
||||||
|
|
||||||
<h2>@Localizer["Delete"]</h2>
|
<h2>@Localizer["Delete"]</h2>
|
||||||
|
|
||||||
|
|
@ -63,4 +66,4 @@
|
||||||
<a asp-action="Index">@Localizer["Back to List"]</a>
|
<a asp-action="Index">@Localizer["Back to List"]</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2,7 +2,12 @@
|
||||||
@using Microsoft.AspNetCore.Html
|
@using Microsoft.AspNetCore.Html
|
||||||
@using System.Text
|
@using System.Text
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Client"];
|
||||||
|
}
|
||||||
|
|
||||||
@functions {
|
@functions {
|
||||||
|
|
||||||
// Lifetimes in IdentityServer are stored as seconds (int). Render them
|
// Lifetimes in IdentityServer are stored as seconds (int). Render them
|
||||||
// in a human-friendly way: "5 min", "2 h", "30 d", "—" for 0/negative.
|
// 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
|
// Zero typically means "use the global default" — show it as such so
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
@model Client
|
@model Client
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Client"];
|
||||||
|
}
|
||||||
|
|
||||||
<h2>@Localizer["Edit"]</h2>
|
<h2>@Localizer["Edit"]</h2>
|
||||||
|
|
||||||
|
|
@ -303,4 +306,4 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<a asp-action="Index">@Localizer["Back to List"]</a>
|
<a asp-action="Index">@Localizer["Back to List"]</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientClaim>
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientClaim>
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Client"];
|
||||||
|
}
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit Client Claims";
|
ViewData["Title"] = "Edit Client Claims";
|
||||||
var clientId = Model.FirstOrDefault()?.ClientId ?? 0;
|
var clientId = Model.FirstOrDefault()?.ClientId ?? 0;
|
||||||
|
|
@ -61,4 +65,4 @@
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a asp-action="Edit" asp-route-id="@clientId">Back to client</a>
|
<a asp-action="Edit" asp-route-id="@clientId">Back to client</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientCorsOrigin>
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientCorsOrigin>
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Client"];
|
||||||
|
}
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit Allowed CORS Origins";
|
ViewData["Title"] = "Edit Allowed CORS Origins";
|
||||||
ViewData["addAction"] = "AddCorsOrigin";
|
ViewData["addAction"] = "AddCorsOrigin";
|
||||||
|
|
@ -23,4 +27,4 @@
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
<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>
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientGrantType>
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Client"];
|
||||||
|
}
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit Allowed Grant Types";
|
ViewData["Title"] = "Edit Allowed Grant Types";
|
||||||
ViewData["addAction"] = "AddGrantType";
|
ViewData["addAction"] = "AddGrantType";
|
||||||
|
|
@ -24,4 +28,4 @@
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
<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>
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientIdPRestriction>
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Client"];
|
||||||
|
}
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit Identity Provider Restrictions";
|
ViewData["Title"] = "Edit Identity Provider Restrictions";
|
||||||
ViewData["addAction"] = "AddIdPRestriction";
|
ViewData["addAction"] = "AddIdPRestriction";
|
||||||
|
|
@ -21,4 +25,4 @@
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
<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>
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri>
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Client"];
|
||||||
|
}
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit Post-logout Redirect URIs";
|
ViewData["Title"] = "Edit Post-logout Redirect URIs";
|
||||||
ViewData["addAction"] = "AddPostLogoutRedirectUri";
|
ViewData["addAction"] = "AddPostLogoutRedirectUri";
|
||||||
|
|
@ -22,4 +26,4 @@
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
<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>
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientProperty>
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Client"];
|
||||||
|
}
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit Client Properties";
|
ViewData["Title"] = "Edit Client Properties";
|
||||||
var clientId = Model.FirstOrDefault()?.ClientId ?? 0;
|
var clientId = Model.FirstOrDefault()?.ClientId ?? 0;
|
||||||
|
|
@ -62,4 +66,4 @@
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a asp-action="Edit" asp-route-id="@clientId">Back to client</a>
|
<a asp-action="Edit" asp-route-id="@clientId">Back to client</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientRedirectUri>
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientRedirectUri>
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Client"];
|
||||||
|
}
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit Redirect URIs";
|
ViewData["Title"] = "Edit Redirect URIs";
|
||||||
ViewData["addAction"] = "AddRedirectUri";
|
ViewData["addAction"] = "AddRedirectUri";
|
||||||
|
|
@ -22,4 +26,4 @@
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
<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>
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientScope>
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Client"];
|
||||||
|
}
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit Allowed Scopes";
|
ViewData["Title"] = "Edit Allowed Scopes";
|
||||||
ViewData["addAction"] = "AddScope";
|
ViewData["addAction"] = "AddScope";
|
||||||
|
|
@ -22,4 +26,4 @@
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
<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>
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientSecret>
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Client"];
|
||||||
|
}
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit Client Secrets";
|
ViewData["Title"] = "Edit Client Secrets";
|
||||||
var clientId = Model.FirstOrDefault()?.ClientId ?? 0;
|
var clientId = Model.FirstOrDefault()?.ClientId ?? 0;
|
||||||
|
|
@ -80,4 +84,4 @@
|
||||||
<p>
|
<p>
|
||||||
<a asp-action="Edit" asp-route-id="@clientId">Back to client</a>
|
<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>
|
<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;
|
@using IdentityServer8.Models;
|
||||||
@model IEnumerable<IdentityServer8.EntityFramework.Entities.Client>
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.Client>
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Client"];
|
||||||
|
}
|
||||||
|
|
||||||
<h2>@Localizer["Index"]</h2>
|
<h2>@Localizer["Index"]</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
@ -10,7 +14,7 @@
|
||||||
@foreach (var item in Model)
|
@foreach (var item in Model)
|
||||||
{
|
{
|
||||||
<div class="list-item">
|
<div class="list-item">
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-title">Identifier</h5>
|
<h5 class="card-title">Identifier</h5>
|
||||||
<h6 class="card-subtitle mb-2 text-muted">and activation</h6>
|
<h6 class="card-subtitle mb-2 text-muted">and activation</h6>
|
||||||
|
|
@ -36,7 +40,7 @@
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
||||||
<h5 class="card-title">Urls</h5>
|
<h5 class="card-title">Urls</h5>
|
||||||
|
|
@ -56,7 +60,7 @@
|
||||||
</dd>
|
</dd>
|
||||||
<dt>@Html.DisplayNameFor(model => model.AbsoluteRefreshTokenLifetime)</dt>
|
<dt>@Html.DisplayNameFor(model => model.AbsoluteRefreshTokenLifetime)</dt>
|
||||||
<dd>@Html.DisplayFor(model => item.AbsoluteRefreshTokenLifetime)</dd>
|
<dd>@Html.DisplayFor(model => item.AbsoluteRefreshTokenLifetime)</dd>
|
||||||
|
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -78,7 +82,7 @@
|
||||||
</dt>
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
@Enum.GetName(typeof(AccessTokenType), item.AccessTokenType)
|
@Enum.GetName(typeof(AccessTokenType), item.AccessTokenType)
|
||||||
|
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</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-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-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>
|
<a class="btn btn-danger btn-lg" asp-action="Delete" asp-route-id="@item.Id">Delete</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
@model Client
|
@model Client
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Client"];
|
||||||
|
}
|
||||||
|
|
||||||
<h2>@Localizer["Regenerate client secret"]</h2>
|
<h2>@Localizer["Regenerate client secret"]</h2>
|
||||||
|
|
||||||
|
|
@ -36,4 +39,4 @@
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<button type="submit" class="btn btn-danger">@Localizer["Regenerate now"]</button>
|
<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>
|
<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 secret = ViewBag.NewClientSecret as string;
|
||||||
var expiresAt = ViewBag.NewClientSecretExpiresAt as string;
|
var expiresAt = ViewBag.NewClientSecretExpiresAt as string;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
@model IEnumerable<CommandForm>
|
@model IEnumerable<CommandForm>
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewBag.Title = "Index";
|
ViewBag.Title = Localizer["CommandForms"];
|
||||||
}
|
}
|
||||||
|
|
||||||
<h2>Index</h2>
|
<h2>@ViewBag.Title</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a asp-action="Create">Create New</a>
|
<a asp-action="Create">Create New</a>
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
</th>
|
</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@foreach (var item in Model) {
|
@foreach (var item in Model) {
|
||||||
<tr class="@(item.Context.Hidden?"disabled":null)">
|
<tr class="@(item.Context.Hidden?"disabled":null)">
|
||||||
<td>
|
<td>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Device"];
|
||||||
|
}
|
||||||
|
|
||||||
<div class="page-device-success">
|
<div class="page-device-success">
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
<h1>Success</h1>
|
<h1>Success</h1>
|
||||||
<p>You have successfully authorized the device</p>
|
<p>You have successfully authorized the device</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
@model string
|
@model string
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Device"];
|
||||||
|
}
|
||||||
|
|
||||||
<div class="page-device-code">
|
<div class="page-device-code">
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
|
|
@ -20,4 +23,4 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
@model DeviceAuthorizationViewModel
|
@model DeviceAuthorizationViewModel
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Device"];
|
||||||
|
}
|
||||||
|
|
||||||
<div class="page-device-confirmation">
|
<div class="page-device-confirmation">
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
|
|
@ -105,4 +108,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
@model DiagnosticsViewModel
|
@model DiagnosticsViewModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Diagnostics"];
|
||||||
|
}
|
||||||
|
|
||||||
<div class="diagnostics-page">
|
<div class="diagnostics-page">
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
<h1>Authentication Cookie</h1>
|
<h1>Authentication Cookie</h1>
|
||||||
|
|
@ -22,7 +26,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
@model Instrumentation
|
@model Instrumentation
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["Do"];
|
||||||
|
}
|
||||||
|
|
||||||
<h2>@Model.Tool.Name</h2>
|
<h2>@Model.Tool.Name</h2>
|
||||||
|
|
||||||
|
|
@ -6,4 +9,4 @@
|
||||||
Remove Instrument</a>
|
Remove Instrument</a>
|
||||||
<form asp-action="AddInstrument" asp-controller="Instrumentation" asp-action-id="Model.UserId" >
|
<form asp-action="AddInstrument" asp-controller="Instrumentation" asp-action-id="Model.UserId" >
|
||||||
<select name="Name" value="" placeholder="Séléctionnez votre instrument" ></select>
|
<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>EstimateToSignAsPro
|
||||||
</dt>
|
</dt>
|
||||||
<dd>@Html.DisplayFor(m=>m.EstimateToSignAsProCount)
|
<dd>@Html.DisplayFor(m=>m.EstimateToHonorAsProCount)
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt>EstimateToSignAsCli
|
<dt>EstimateToSignAsCli
|
||||||
|
|
@ -21,11 +21,6 @@
|
||||||
<dd>@Html.DisplayFor(m=>m.EstimateToSignAsCliCount)
|
<dd>@Html.DisplayFor(m=>m.EstimateToSignAsCliCount)
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt>BillToSignAsPro
|
|
||||||
</dt>
|
|
||||||
<dd>@Html.DisplayFor(m=>m.BillToSignAsProCount)
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
<dt>BillToSignAsCli
|
<dt>BillToSignAsCli
|
||||||
</dt>
|
</dt>
|
||||||
<dd>@Html.DisplayFor(m=>m.BillToSignAsCliCount)
|
<dd>@Html.DisplayFor(m=>m.BillToSignAsCliCount)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
@model Yavsc.Models.Haircut.BrusherProfile
|
@model Yavsc.Models.Haircut.BrusherProfile
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Localizer["FrontOffice"];
|
||||||
|
}
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Disponibilités</legend>
|
<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