Compare commits
No commits in common. "f2ae01729a821bef023627b9439f8e5447a499b8" and "ec54108cf081fe22c9186265064eacbca427b0cf" have entirely different histories.
f2ae01729a
...
ec54108cf0
828 changed files with 217113 additions and 7814 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
**/bin/
|
**/bin/
|
||||||
**/obj/
|
**/obj/
|
||||||
**/.playwright/
|
**/.playwright/
|
||||||
|
.git/
|
||||||
.vs/
|
.vs/
|
||||||
.github/
|
.github/
|
||||||
# Exclure uniquement les dossiers de sortie de compilation
|
# Exclure uniquement les dossiers de sortie de compilation
|
||||||
|
|
@ -13,4 +14,5 @@ test/*/obj/
|
||||||
|
|
||||||
# Exclure les caches lourds
|
# Exclure les caches lourds
|
||||||
**/.playwright/
|
**/.playwright/
|
||||||
|
.git/
|
||||||
.vs/
|
.vs/
|
||||||
|
|
|
||||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
|
|
@ -59,7 +59,7 @@ jobs:
|
||||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Add any setup steps before running the `github/codeql-action/init` action.
|
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||||
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||||
|
|
|
||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -14,7 +14,7 @@
|
||||||
"name": "Yavsc.Org",
|
"name": "Yavsc.Org",
|
||||||
"type": "dotnet",
|
"type": "dotnet",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"projectPath": "${workspaceFolder}/src/Yavsc.Org/Yavsc.Org.csproj",
|
"projectPath": "${workspaceFolder}/src/Yavsc.Org/Yavsc.Org.csproj"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Yavsc.Blogs",
|
"name": "Yavsc.Blogs",
|
||||||
|
|
|
||||||
11
.vscode/mcp.json
vendored
11
.vscode/mcp.json
vendored
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"servers": {
|
|
||||||
"openclaw": {
|
|
||||||
"type": "stdio",
|
|
||||||
"command": "/home/paul/.nvm/versions/node/v22.23.0/bin/node",
|
|
||||||
"args": [
|
|
||||||
"/home/paul/Workspace/tools/openclaw-mcp-server.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
.vscode/settings.json
vendored
14
.vscode/settings.json
vendored
|
|
@ -26,17 +26,5 @@
|
||||||
"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",
|
|
||||||
"copilotcli/gpt-5.3-codex"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,20 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<!--
|
||||||
|
Shared package versions: declared in two-or-more top-level products in src/.
|
||||||
|
|
||||||
|
Each product directory (src/<Product>/) has its own Directory.Packages.props
|
||||||
|
that <Import>s this file via GetPathOfFileAbove and adds the
|
||||||
|
product-specific versions. Adding a new shared package means editing this
|
||||||
|
file only; adding a product-local package means editing the per-product
|
||||||
|
Directory.Packages.props only.
|
||||||
|
-->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
|
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
|
||||||
<PackageVersion Include="GitVersion.MsBuild" Version="6.8.1" />
|
<PackageVersion Include="GitVersion.MsBuild" Version="6.7.0" />
|
||||||
<PackageVersion Include="HigginsSoft.IdentityServer8" Version="8.1.0-alpha.171" />
|
<PackageVersion Include="HigginsSoft.IdentityServer8" Version="8.0.5-preview-net9" />
|
||||||
<PackageVersion Include="HigginsSoft.IdentityServer8.EntityFramework" Version="8.1.0-alpha.171" />
|
<PackageVersion Include="HigginsSoft.IdentityServer8.EntityFramework" Version="8.0.5-preview-net9" />
|
||||||
<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" />
|
||||||
|
|
@ -15,7 +24,7 @@
|
||||||
<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.7.0" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.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" />
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ COPY . .
|
||||||
|
|
||||||
# (3) Source NuGet interne (Letsencrypt, certificat auto-signé côté
|
# (3) Source NuGet interne (Letsencrypt, certificat auto-signé côté
|
||||||
# serveur, justifié par build privé).
|
# serveur, justifié par build privé).
|
||||||
RUN dotnet nuget add source https://isn.pschneider.fr/api/v3/index.json --allow-insecure-connections
|
RUN dotnet nuget add source https://isn.pschneider.fr/v3/index.json --allow-insecure-connections
|
||||||
|
|
||||||
# (4) Restore
|
# (4) Restore
|
||||||
RUN dotnet restore
|
RUN dotnet restore
|
||||||
|
|
|
||||||
|
|
@ -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/api/v3/index.json
|
RUN dotnet nuget add source https://isn.pschneider.fr/v3/index.json --allow-insecure-connections
|
||||||
|
|
||||||
# 4. Restauration des dépendances pour tous les projets
|
# 4. Restauration des dépendances pour tous les projets
|
||||||
RUN dotnet restore
|
RUN dotnet restore
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -11,7 +11,7 @@ all:
|
||||||
dotnet build --nologo
|
dotnet build --nologo
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
dotnet clean -c $(CONFIG)
|
dotnet clean
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -152,13 +152,6 @@ d'abord `appsettings-org.json` du serveur ; sinon, laisse-le en place.
|
||||||
(utilisateur, mot de passe, hôte, base). Privilégier
|
(utilisateur, mot de passe, hôte, base). Privilégier
|
||||||
`dotnet user-secrets` ou des variables d'environnement `ASPNETCORE_*`
|
`dotnet user-secrets` ou des variables d'environnement `ASPNETCORE_*`
|
||||||
plutôt qu'un mot de passe en clair dans le fichier.
|
plutôt qu'un mot de passe en clair dans le fichier.
|
||||||
- Au démarrage, Yavsc.Org applique automatiquement ses migrations EF
|
|
||||||
Core. Sur cette base de code, EF Core 10 peut encore lever un
|
|
||||||
`PendingModelChangesWarning` malgré des migrations et snapshots déjà
|
|
||||||
alignés ; ce faux positif est ignoré sur les contextes PostgreSQL pour
|
|
||||||
éviter un démarrage inutilement en mode dégradé. Si une erreur de
|
|
||||||
migration apparaît encore en production, elle doit être traitée comme
|
|
||||||
une vraie divergence de schéma ou de connexion.
|
|
||||||
- `Smtp.*` — hôte, port, identifiants SMTP pour l'envoi d'e-mails
|
- `Smtp.*` — hôte, port, identifiants SMTP pour l'envoi d'e-mails
|
||||||
transactionnels.
|
transactionnels.
|
||||||
- `Authentication.PayPal.*` et `Authentication.Google.*` — clés d'API
|
- `Authentication.PayPal.*` et `Authentication.Google.*` — clés d'API
|
||||||
|
|
|
||||||
278
doc/dev-tracking/client-editor-overhaul.md
Normal file
278
doc/dev-tracking/client-editor-overhaul.md
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
# Client editor overhaul — Yavsc.Org administration
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Bring the OAuth2 client administration UI (`/Client/Edit/{id}` and friends)
|
||||||
|
in Yavsc.Org to feature parity with the IdentityServer8 `Client` entity
|
||||||
|
model. Today the editor only exposes a handful of scalar fields and a few
|
||||||
|
single-line inputs for collections; the bulk of the entity and its
|
||||||
|
related collections are unreachable from the UI.
|
||||||
|
|
||||||
|
## Inventory — current state
|
||||||
|
|
||||||
|
### Properties exposed by `Views/Client/Edit.cshtml`
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
| ------------------------ | ----------- | ---------------------------------- |
|
||||||
|
| `ClientId` | string | hidden, identifier |
|
||||||
|
| `Enabled` | bool | checkbox |
|
||||||
|
| `ClientName` | string | display name |
|
||||||
|
| `FrontChannelLogoutUri` | string | only front-channel, no back-channel |
|
||||||
|
| `RedirectUris` | collection | rendered as a single text input |
|
||||||
|
| `IdentityTokenLifetime` | int | seconds |
|
||||||
|
| `AbsoluteRefreshTokenLifetime` | int | seconds |
|
||||||
|
| `ClientSecrets` | collection | rendered as a single text input |
|
||||||
|
| `AccessTokenType` | enum | dropdown (custom `SetAppTypesInputValues`) |
|
||||||
|
|
||||||
|
### Properties of `IdentityServer8.EntityFramework.Entities.Client` **NOT** in the editor
|
||||||
|
|
||||||
|
Core scalars (16 fields missing):
|
||||||
|
|
||||||
|
- `Description`
|
||||||
|
- `ClientUri`
|
||||||
|
- `LogoUri`
|
||||||
|
- `RequireConsent`
|
||||||
|
- `RequirePkce`
|
||||||
|
- `RequireRequestObject`
|
||||||
|
- `RequireClientSecret`
|
||||||
|
- `AllowPlainTextPkce`
|
||||||
|
- `AllowOfflineAccess`
|
||||||
|
- `AllowRememberConsent`
|
||||||
|
- `AlwaysIncludeUserClaimsInIdToken`
|
||||||
|
- `AlwaysSendClientClaims`
|
||||||
|
- `AuthorizationCodeLifetime`
|
||||||
|
- `BackChannelLogoutUri`
|
||||||
|
- `BackChannelLogoutSessionRequired`
|
||||||
|
- `CibaLifetime`
|
||||||
|
- `ClientClaimsPrefix`
|
||||||
|
- `ConsentLifetime`
|
||||||
|
- `Created`
|
||||||
|
- `DeviceCodeLifetime`
|
||||||
|
- `EnableLocalLogin`
|
||||||
|
- `Enabled`
|
||||||
|
- `FrontChannelLogoutSessionRequired`
|
||||||
|
- `IncludeJwtId`
|
||||||
|
- `LastAccessed`
|
||||||
|
- `LogoUri`
|
||||||
|
- `NonEditable`
|
||||||
|
- `PairwiseSubjectSalt`
|
||||||
|
- `PollingInterval`
|
||||||
|
- `ProtocolType`
|
||||||
|
- `RefreshTokenExpiration`
|
||||||
|
- `RefreshTokenUsage`
|
||||||
|
- `SlidingRefreshTokenLifetime`
|
||||||
|
- `UpdateAccessTokenClaimsOnRefresh`
|
||||||
|
- `Updated`
|
||||||
|
- `UserCodeType`
|
||||||
|
- `UserSsoLifetime`
|
||||||
|
|
||||||
|
Collections (8 missing — currently either not exposed at all, or jammed
|
||||||
|
into a single-line text input that doesn't work for an IEnumerable):
|
||||||
|
|
||||||
|
- `AllowedGrantTypes` → `ClientGrantType` (GrantType)
|
||||||
|
- `AllowedScopes` → `ClientScope` (Scope)
|
||||||
|
- `RedirectUris` → `ClientRedirectUri` (RedirectUri) — exposed but broken
|
||||||
|
- `PostLogoutRedirectUris` → `ClientPostLogoutRedirectUri` (PostLogoutRedirectUri)
|
||||||
|
- `AllowedCorsOrigins` → `ClientCorsOrigin` (Origin)
|
||||||
|
- `IdentityProviderRestrictions` → `ClientIdPRestriction` (Provider)
|
||||||
|
- `Claims` → `ClientClaim` (Type, Value)
|
||||||
|
- `Properties` → `ClientProperty` (Key, Value)
|
||||||
|
- `ClientSecrets` → `ClientSecret` (Type, Value, Description, Created, Expiration) — exposed but broken
|
||||||
|
- `AllowedSigningAlgorithms` → scalar string collection on Client itself
|
||||||
|
|
||||||
|
## Pages to add
|
||||||
|
|
||||||
|
Pattern: one Razor page per collection under
|
||||||
|
`Views/Client/Edit{Collection}.cshtml`. Each page lists existing rows,
|
||||||
|
offers an "Add" form with the relevant fields, and a per-row
|
||||||
|
remove button. The main `Edit.cshtml` becomes a hub page with links
|
||||||
|
to each subpage plus the scalar fields it already has.
|
||||||
|
|
||||||
|
| Page | Route | Form fields |
|
||||||
|
| ------------------------------------- | ------------------------------------------ | ------------------------------------------------- |
|
||||||
|
| `Edit.cshtml` | `GET /Client/Edit/{id}` (existing) | scalar fields + nav links |
|
||||||
|
| `EditRedirectUris.cshtml` | `GET /Client/EditRedirectUris/{id}` | `RedirectUri` |
|
||||||
|
| `EditPostLogoutRedirectUris.cshtml` | `GET /Client/EditPostLogoutRedirectUris/{id}` | `PostLogoutRedirectUri` |
|
||||||
|
| `EditScopes.cshtml` | `GET /Client/EditScopes/{id}` | `Scope` (with select of known scopes) |
|
||||||
|
| `EditGrantTypes.cshtml` | `GET /Client/EditGrantTypes/{id}` | `GrantType` (with select of known types) |
|
||||||
|
| `EditCorsOrigins.cshtml` | `GET /Client/EditCorsOrigins/{id}` | `Origin` |
|
||||||
|
| `EditIdPRestrictions.cshtml` | `GET /Client/EditIdPRestrictions/{id}` | `Provider` |
|
||||||
|
| `EditClaims.cshtml` | `GET /Client/EditClaims/{id}` | `Type`, `Value` |
|
||||||
|
| `EditProperties.cshtml` | `GET /Client/EditProperties/{id}` | `Key`, `Value` |
|
||||||
|
| `EditSecrets.cshtml` (replacement) | `GET /Client/EditSecrets/{id}` | `Type`, `Value`, `Description`, `Expiration` |
|
||||||
|
|
||||||
|
Partial view `_EditableList.cshtml` factored once and consumed by all
|
||||||
|
of the above.
|
||||||
|
|
||||||
|
## Controller actions to add
|
||||||
|
|
||||||
|
For each collection `Foo`:
|
||||||
|
|
||||||
|
- `GET EditFoo(int id)` — load the client, render the page
|
||||||
|
- `POST AddFoo(int id, …)` — append a row, redirect to `EditFoo`
|
||||||
|
- `POST RemoveFoo(int id, int rowId)` — delete a row, redirect
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `dotnet build src/Yavsc.Org/Yavsc.Org.csproj` → 0 errors
|
||||||
|
- No tests in `Yavsc.Org.Tests` exercise the controller today (per
|
||||||
|
`find … -name "ClientController*" -not -path "*/bin/*"`). Smoke-test
|
||||||
|
by logging in as admin, hitting `/Client/Edit/1`, then each
|
||||||
|
`Edit*/1` page, and verifying the add/remove POSTs.
|
||||||
|
- Existing seed flow (`MigratePostItClientToPublic` in
|
||||||
|
`HostingExtensions.cs`) must keep working — the editor changes are
|
||||||
|
additive, not destructive.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Tests (no MVC test infrastructure currently exists for this controller)
|
||||||
|
- Migration of existing collection fields (the broken `RedirectUris`
|
||||||
|
text input will simply be replaced by the new subpage)
|
||||||
|
- Per-collection authorization policies (the controller is already
|
||||||
|
`[Authorize("AdministratorOnly")]`)
|
||||||
|
- Client cloning / templating / JSON import-export
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
2026-06-21 16:04 — kickoff. Inventory done. Pages not yet started.
|
||||||
|
|
||||||
|
2026-06-21 16:11 — first delivery, **build does not compile by design**
|
||||||
|
(per Paul: "Tu peux même me laisser un travail qui ne compile
|
||||||
|
pas"). The structural work is done; the residual errors are easy
|
||||||
|
fixes Paul will do in a debug session.
|
||||||
|
|
||||||
|
Files added (working tree, not yet committed):
|
||||||
|
|
||||||
|
- `src/Yavsc.Org/Controllers/Administration/ClientController.Collections.cs`
|
||||||
|
— partial class with the per-collection GET / Add / Remove actions.
|
||||||
|
- `src/Yavsc.Org/Views/Client/EditRedirectUris.cshtml`
|
||||||
|
- `src/Yavsc.Org/Views/Client/EditPostLogoutRedirectUris.cshtml`
|
||||||
|
- `src/Yavsc.Org/Views/Client/EditScopes.cshtml`
|
||||||
|
- `src/Yavsc.Org/Views/Client/EditGrantTypes.cshtml`
|
||||||
|
- `src/Yavsc.Org/Views/Client/EditCorsOrigins.cshtml`
|
||||||
|
- `src/Yavsc.Org/Views/Client/EditIdPRestrictions.cshtml`
|
||||||
|
- `src/Yavsc.Org/Views/Client/EditClaims.cshtml`
|
||||||
|
- `src/Yavsc.Org/Views/Client/EditProperties.cshtml`
|
||||||
|
- `src/Yavsc.Org/Views/Client/EditSecrets.cshtml`
|
||||||
|
- `src/Yavsc.Org/Views/Client/_EditableStringList.cshtml`
|
||||||
|
— partial consumed by the single-string-field collection pages.
|
||||||
|
|
||||||
|
Files modified:
|
||||||
|
|
||||||
|
- `src/Yavsc.Org/Controllers/Administration/ClientController.cs`
|
||||||
|
— `class` → `partial class`; the `Edit(int id)` GET now uses
|
||||||
|
`LoadClientAsync` to load all navigations (so the new Edit.cshtml
|
||||||
|
can render counts in its nav links).
|
||||||
|
- `src/Yavsc.Org/Views/Client/Edit.cshtml`
|
||||||
|
— significantly enriched: nav links to the 9 sub-pages, all the
|
||||||
|
scalar fields split into fieldsets (Security, Logout, Tokens,
|
||||||
|
Device / CIBA, Tokens-extra), ClientId / Id hidden.
|
||||||
|
|
||||||
|
### Known residual compile errors (4 errors total)
|
||||||
|
|
||||||
|
Paul is fixing these in a debug session. The structure is sound; the
|
||||||
|
errors are missing properties on the `Client` entity, a Razor
|
||||||
|
nullable quirk, and a `Localizer` injection miss.
|
||||||
|
|
||||||
|
1. `Edit.cshtml:249` — `PairwiseSubjectSalt` doesn't exist on
|
||||||
|
`IdentityServer8.EntityFramework.Entities.Client`. **Fix**: drop
|
||||||
|
the field from Edit.cshtml; IdentityServer8 likely uses a
|
||||||
|
different property name (e.g. on a related entity) or doesn't
|
||||||
|
expose it.
|
||||||
|
2. `Edit.cshtml:221` — `CibaLifetime` doesn't exist on `Client`.
|
||||||
|
**Fix**: same as above. CIBA flow may be configured elsewhere
|
||||||
|
(resource-level) or via a different property.
|
||||||
|
3. `ClientController.Collections.cs` lines 181, 217, 253, 304 —
|
||||||
|
`Localizer` is not available in the partial class. **Fix**: inject
|
||||||
|
`IStringLocalizer<ClientController>` via the constructor, or
|
||||||
|
inline the strings ("BothTypeAndValueRequired", "KeyRequired",
|
||||||
|
"ValueRequired", "SecretValueRequired").
|
||||||
|
4. `EditSecrets.cshtml:44` — `s.Expiration?.ToString("u")` on a
|
||||||
|
`DateTime?`. **Fix**: just `s.Expiration?.ToString("u")` works
|
||||||
|
if you write `s.Expiration.Value.ToString("u")`, or use
|
||||||
|
`(s.Expiration is null ? "" : s.Expiration.Value.ToString("u"))`,
|
||||||
|
or `s.Expiration?.ToString("u") ?? string.Empty`.
|
||||||
|
|
||||||
|
### Suggested next session
|
||||||
|
|
||||||
|
Once the 4 compile errors are fixed and the pages render:
|
||||||
|
|
||||||
|
1. Smoke test by logging in as admin, hitting `/Client/Edit/1`,
|
||||||
|
then each `Edit*/1` page, and verifying add/remove POSTs.
|
||||||
|
2. Add a confirmation prompt (or 2-step form) for Remove actions —
|
||||||
|
removing a Redirect URI is destructive and one click is too easy.
|
||||||
|
3. Wire up some collection-level validation (e.g. redirect URI must
|
||||||
|
be a valid URL) at the controller level.
|
||||||
|
4. Add tests — the project doesn't have MVC test infrastructure
|
||||||
|
today; consider adding a `Yavsc.Org.Tests` project that drives
|
||||||
|
the controller via `WebApplicationFactory<Program>`.
|
||||||
|
|
||||||
|
|
||||||
|
## Test bootstrap notes (session of 2026-06-21 17:00+)
|
||||||
|
|
||||||
|
When adding new integration tests against `WebServerFixture`:
|
||||||
|
|
||||||
|
1. **Skip `/Account/Login` roundtrip.** The fixture ships without
|
||||||
|
`MapRazorPages()` (commented out in `HostingExtensions.ConfigurePipeline`),
|
||||||
|
so `/Identity/Account/Login` is 404, and the custom
|
||||||
|
`/Account/Login` route requires a complex antiforgery dance.
|
||||||
|
Instead, build a `ClaimsPrincipal` for the test user via
|
||||||
|
`UserManager` + `IUserClaimsPrincipalFactory<ApplicationUser>`,
|
||||||
|
then call `IAuthenticationService.SignInAsync` on a synthetic
|
||||||
|
`DefaultHttpContext` and replay the resulting `Set-Cookie` header
|
||||||
|
into the test `HttpClient`. See
|
||||||
|
`ClientControllerCollectionTests.IssueIdentityCookie`.
|
||||||
|
|
||||||
|
2. **Create the `Administrator` role before assigning it.** ASP.NET
|
||||||
|
Identity stores roles in `AspNetRoles`; there is no automatic seed.
|
||||||
|
The constant name is `YavscConstants.AdminGroupName` = `"Administrator"`.
|
||||||
|
Use `RoleManager<IdentityRole>.CreateAsync(new IdentityRole("Administrator"))`
|
||||||
|
before `AddToRoleAsync`.
|
||||||
|
|
||||||
|
3. **Use `InMemory` connection string to bypass the prod signing-cert
|
||||||
|
requirement.** `HostingExtensions.AddIdentityServer` requires a
|
||||||
|
PEM cert unless `builder.Environment.IsDevelopment()` OR
|
||||||
|
`UsesInMemoryProvider(connectionString)`. The fixture already
|
||||||
|
uses `InMemory`, so `AddDeveloperSigningCredential()` is called
|
||||||
|
automatically — but only after we wired this check in (see
|
||||||
|
commit history).
|
||||||
|
|
||||||
|
4. **Field-name gotchas** (from disassembling HigginsSoft
|
||||||
|
IdentityServer8.EntityFramework.Entities.Client 8.0.5-preview-net9):
|
||||||
|
- `PairWiseSubjectSalt` (capital W on "Wise"), not `PairwiseSubjectSalt`.
|
||||||
|
- `CibaLifetime` and `PollingInterval` do NOT exist on `Client` in
|
||||||
|
this version.
|
||||||
|
- `ConsentLifetime` and `UserSsoLifetime` are `int?`.
|
||||||
|
|
||||||
|
5. **`MapStaticAssets()` fails on test projects.** Calling
|
||||||
|
`MapStaticAssets()` resolves a manifest file
|
||||||
|
(`<project>.staticwebassets.endpoints.json`) that test projects
|
||||||
|
don't produce. Skip when `WebRootPath` points at the test
|
||||||
|
assembly directory.
|
||||||
|
|
||||||
|
6. **Routing 404 on /Client/Edit/{id} via WebServerFixture.** As of
|
||||||
|
this session, the GET endpoint returns 404 even with admin
|
||||||
|
header. The route mapping is intact
|
||||||
|
(`MapDefaultControllerRoute()`), so this is likely an MVC
|
||||||
|
convention routing issue with the
|
||||||
|
`Controllers/Administration/` subdirectory. To investigate
|
||||||
|
next session: log middleware pipeline or hit `/Client` index
|
||||||
|
first to see if any Client route resolves.
|
||||||
|
|
||||||
|
7. **`MapStaticAssets()` is unconditional in prod, but blocks tests.**
|
||||||
|
`WebApplication.CreateBuilder` defaults `ContentRootPath` to
|
||||||
|
`AppContext.BaseDirectory`. In test runs that resolves to
|
||||||
|
`src/Yavsc.Org.Tests/bin/Debug/net10.0/`, where
|
||||||
|
`Yavsc.Org.Tests.staticwebassets.endpoints.json` doesn't exist
|
||||||
|
(it's generated only by projects with the Web SDK). The
|
||||||
|
`app.MapStaticAssets()` call inside `ConfigurePipeline` then
|
||||||
|
throws and the fixture fails to start — taking every test in
|
||||||
|
the `[Collection("Yavsc Server")]` down with it.
|
||||||
|
|
||||||
|
This is a pre-existing fragility of the WebServerFixture that
|
||||||
|
the new test work surfaced. Fixing it cleanly requires either:
|
||||||
|
(a) moving the test project to the Web SDK so it produces its
|
||||||
|
own manifest, (b) copying the manifest at build time via an
|
||||||
|
MSBuild target, or (c) routing `MapStaticAssets` through an
|
||||||
|
assembly-resolution fallback. None attempted in this session —
|
||||||
|
recorded for next session.
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,188 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,163 +0,0 @@
|
||||||
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", TestContext.Current.CancellationToken);
|
HttpMethod.Get, "posts");
|
||||||
|
|
||||||
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", TestContext.Current.CancellationToken);
|
HttpMethod.Get, "posts");
|
||||||
|
|
||||||
Assert.NotEmpty(posts);
|
Assert.NotEmpty(posts);
|
||||||
Assert.Equal(2, apiServer.RequestCount);
|
Assert.Equal(2, apiServer.RequestCount);
|
||||||
|
|
@ -125,15 +125,14 @@ 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();
|
||||||
|
|
||||||
|
|
@ -155,7 +154,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
|
||||||
{
|
{
|
||||||
|
|
@ -168,7 +167,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));
|
||||||
|
|
@ -182,7 +181,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(
|
||||||
|
|
@ -215,7 +214,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
|
||||||
|
|
@ -226,7 +225,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();
|
||||||
|
|
||||||
|
|
@ -235,18 +234,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
|
||||||
{
|
{
|
||||||
|
|
@ -257,24 +256,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, TestContext.Current.CancellationToken));
|
() => client.LoginInteractiveAsync(progress));
|
||||||
// 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
|
||||||
{
|
{
|
||||||
|
|
@ -285,7 +284,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();
|
||||||
|
|
||||||
|
|
@ -294,7 +293,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(null, TestContext.Current.CancellationToken);
|
var ok = await client.TrySilentLoginAsync();
|
||||||
Assert.False(ok);
|
Assert.False(ok);
|
||||||
Assert.False(client.HasValidSession);
|
Assert.False(client.HasValidSession);
|
||||||
}
|
}
|
||||||
|
|
@ -302,7 +301,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();
|
||||||
|
|
||||||
|
|
@ -315,7 +314,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(null, TestContext.Current.CancellationToken);
|
var ok = await client.TrySilentLoginAsync();
|
||||||
Assert.True(ok);
|
Assert.True(ok);
|
||||||
Assert.True(client.HasValidSession);
|
Assert.True(client.HasValidSession);
|
||||||
}
|
}
|
||||||
|
|
@ -328,7 +327,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();
|
||||||
|
|
||||||
|
|
@ -352,13 +351,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, TestContext.Current.CancellationToken);
|
var ok = await client.TrySilentLoginAsync(progress);
|
||||||
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
|
||||||
{
|
{
|
||||||
|
|
@ -431,7 +430,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,7 +63,6 @@ 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);
|
||||||
|
|
@ -73,7 +72,6 @@ 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.
|
||||||
|
|
|
||||||
|
|
@ -1,204 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
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,16 +205,6 @@ 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,
|
||||||
|
|
@ -226,16 +216,6 @@ 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,7 +29,6 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
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,17 +35,6 @@
|
||||||
<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,8 +1,5 @@
|
||||||
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;
|
||||||
|
|
||||||
|
|
@ -13,30 +10,4 @@ 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -19,6 +19,6 @@ namespace Yavsc.Abstract.Chat
|
||||||
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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -19,23 +19,20 @@
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
// You should have received a copy of the GNU Lesser General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
using Microsoft.Extensions.Localization;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Yavsc;
|
||||||
|
|
||||||
namespace Yavsc.Server.Hubs
|
namespace Yavsc
|
||||||
{
|
{
|
||||||
public class HubInputValidator {
|
public class HubInputValidator {
|
||||||
|
|
||||||
public IStringLocalizer _locator;
|
|
||||||
public HubInputValidator(IStringLocalizer locator) {
|
|
||||||
_locator = locator;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Action<string,string,string> NotifyUser {get;set;}
|
public Action<string,string,string> NotifyUser {get;set;}
|
||||||
public bool ValidateRoomName (string roomName)
|
public bool ValidateRoomName (string roomName)
|
||||||
{
|
{
|
||||||
bool valid = ValidateStringLength(roomName,1,25);
|
bool valid = ValidateStringLength(roomName,1,25);
|
||||||
if (valid) valid = IsLetterOrDigit(roomName);
|
if (valid) valid = IsLetterOrDigit(roomName);
|
||||||
if (!valid) NotifyUser(NotificationTypes.Error, "roomName", _locator.GetString("InvalidRoomName"));
|
if (!valid) NotifyUser(NotificationTypes.Error, "roomName", ChatHubLabels.InvalidRoomName);
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
public bool ValidateUserName (string userName)
|
public bool ValidateUserName (string userName)
|
||||||
|
|
@ -48,18 +45,14 @@ namespace Yavsc.Server.Hubs
|
||||||
if (valid) valid = ValidateStringLength(suname, 1,12);
|
if (valid) valid = ValidateStringLength(suname, 1,12);
|
||||||
if (valid) valid = IsLetterOrDigit(userName);
|
if (valid) valid = IsLetterOrDigit(userName);
|
||||||
}
|
}
|
||||||
if (!valid)
|
if (!valid) NotifyUser(NotificationTypes.Error, "userName" , ChatHubLabels.InvalidUserName);
|
||||||
NotifyUser(
|
|
||||||
NotificationTypes.Error,
|
|
||||||
"userName",
|
|
||||||
_locator.GetString("InvalidUserName)"));
|
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
public bool ValidateMessage (string message)
|
public bool ValidateMessage (string message)
|
||||||
{
|
{
|
||||||
if (!ValidateStringLength(message, 1, 10240))
|
if (!ValidateStringLength(message, 1, 10240))
|
||||||
{
|
{
|
||||||
NotifyUser(NotificationTypes.Error, "message", _locator.GetString("InvalidMessage"));
|
NotifyUser(NotificationTypes.Error, "message", ChatHubLabels.InvalidMessage);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -68,7 +61,7 @@ namespace Yavsc.Server.Hubs
|
||||||
{
|
{
|
||||||
if (!ValidateStringLength(reason, 1,240))
|
if (!ValidateStringLength(reason, 1,240))
|
||||||
{
|
{
|
||||||
NotifyUser(NotificationTypes.Error, "reason", "InvalidReason");
|
NotifyUser(NotificationTypes.Error, "reason", ChatHubLabels.InvalidReason);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -26,7 +26,7 @@ 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<IQuery> GetBillAsync(string billingCode, long queryId);
|
Task<IDecidableQuery> 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
|
||||||
|
|
|
||||||
82
src/Yavsc.Abstract/Resources/Yavsc.ChatHubLabels.Designer.cs
generated
Normal file
82
src/Yavsc.Abstract/Resources/Yavsc.ChatHubLabels.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
// ------------------------------------------------------------------------------
|
||||||
|
// <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,6 +4,8 @@ namespace Yavsc.Abstract.Workflow
|
||||||
{
|
{
|
||||||
public interface IDecidableQuery: ITrackedEntity, IQuery
|
public interface IDecidableQuery: ITrackedEntity, IQuery
|
||||||
{
|
{
|
||||||
|
bool Decided { get; set; }
|
||||||
|
bool Accepted { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,12 @@ 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,8 +5,6 @@ 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
|
||||||
{
|
{
|
||||||
|
|
@ -183,203 +181,5 @@ 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; }
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ namespace Yavsc.Controllers
|
||||||
Location = c.Location,
|
Location = c.Location,
|
||||||
EventDate = c.EventDate,
|
EventDate = c.EventDate,
|
||||||
Id = c.Id,
|
Id = c.Id,
|
||||||
Previsional = c.Provisional,
|
Previsional = c.Previsional,
|
||||||
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.Status = QueryStatus.Rejected;
|
billing.Accepted = false;
|
||||||
dbContext.SaveChanges();
|
dbContext.SaveChanges();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +47,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.Status = QueryStatus.Accepted;
|
billing.Accepted = true;
|
||||||
|
billing.Decided = true;
|
||||||
dbContext.SaveChanges();
|
dbContext.SaveChanges();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.Regularization)
|
Include(q => q.Client.PostalAddress).Include(q => q.Prestation).Include(q=>q.Regularisation)
|
||||||
.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
|
||||||
{
|
{
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
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,8 +36,8 @@ namespace Yavsc
|
||||||
{
|
{
|
||||||
WorkflowHelpers.ConfigureBillingService();
|
WorkflowHelpers.ConfigureBillingService();
|
||||||
|
|
||||||
var firstRegistrar = new Func<ApplicationDbContext, long, IQuery>((db, id) =>
|
var firstRegistrar = new Func<ApplicationDbContext, long, IDecidableQuery>((db, id) =>
|
||||||
db.HairCutQueries.Include(q => q.Prestation).Include(q => q.Regularization).Single(q => q.Id == id));
|
db.HairCutQueries.Include(q => q.Prestation).Include(q => q.Regularisation).Single(q => q.Id == id));
|
||||||
|
|
||||||
const string testCode = "Brush";
|
const string testCode = "Brush";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,20 @@
|
||||||
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 Microsoft.Extensions.Localization;
|
using Yavsc.Helpers;
|
||||||
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
|
||||||
|
|
@ -51,14 +47,11 @@ 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 = BillingService.Billing.Keys
|
ViewBag.ActionName = _context.CommandForm.Select(c => new SelectListItem { Value = c.Id.ToString(), Text = c.Title, Selected = commandForm.Id == c.Id });
|
||||||
.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.UtcNow;
|
var now = DateTime.Now;
|
||||||
|
|
||||||
var model = new FrontOfficeIndexViewModel
|
var model = new FrontOfficeIndexViewModel
|
||||||
{
|
{
|
||||||
EstimateToProduceCount = _context.RdvQueries.Where(c => c.PerformerId == uid && c.EventDate > now && c.Status == QueryStatus.Inserted
|
EstimateToProduceCount = _context.RdvQueries.Where(c => c.PerformerId == uid && c.EventDate > now
|
||||||
&& c.ValidationDate == null && !_context.Estimates.Any(e => e.CommandId == c.Id)).Count(),
|
&& c.ValidationDate == null && !_context.Estimates.Any(e => (e.CommandId == c.Id && e.ProviderValidationDate != null))).Count(),
|
||||||
EstimateToHonorAsProCount = _context.RdvQueries.Where(c => c.PerformerId == uid && c.EventDate > now && c.Status == QueryStatus.Accepted
|
EstimateToSignAsProCount = _context.RdvQueries.Where(c => (c.PerformerId == uid && c.EventDate > now
|
||||||
&& c.ValidationDate == null && _context.Estimates.Any(e => e.CommandId == c.Id )).Count(),
|
&& c.ValidationDate == null && _context.Estimates.Any(e => (e.CommandId == c.Id && e.ProviderValidationDate != null)))).Count(),
|
||||||
EstimateToSignAsCliCount = _context.Estimates.Where(e => e.ClientId == uid && e.Query.Status == QueryStatus.Accepted).Count(),
|
EstimateToSignAsCliCount = _context.Estimates.Where(e => e.ClientId == uid && e.ClientValidationDate == null).Count(),
|
||||||
|
BillToSignAsProCount = 0,
|
||||||
BillToSignAsCliCount = 0,
|
BillToSignAsCliCount = 0,
|
||||||
NewPayementsCount = 0
|
NewPayementsCount = 0
|
||||||
};
|
};
|
||||||
|
|
@ -65,14 +65,14 @@ namespace Yavsc.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task <ActionResult> ListPerformersAsync(string activityCode)
|
public async Task <ActionResult> HairCut(string id)
|
||||||
{
|
{
|
||||||
if (activityCode == null)
|
if (id == null)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException("No Activity code");
|
throw new NotImplementedException("No Activity code");
|
||||||
}
|
}
|
||||||
ViewBag.Activity = await _context.Activities.FirstOrDefaultAsync(a => a.Code == activityCode);
|
ViewBag.Activity = await _context.Activities.FirstOrDefaultAsync(a => a.Code == id);
|
||||||
var result = await _context.ListPerformersAsync(_billing, activityCode);
|
var result = await _context.ListPerformersAsync(_billing, id);
|
||||||
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.MusicLoverSettings.ToListAsync());
|
return View(await _context.GeneralSettings.ToListAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: GeneralSettings/Details/5
|
// GET: GeneralSettings/Details/5
|
||||||
|
|
@ -28,7 +28,7 @@ namespace Yavsc.Controllers
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
MusicLoverSettings generalSettings = await _context.MusicLoverSettings.SingleAsync(m => m.UserId == id);
|
MusicLoverSettings generalSettings = await _context.GeneralSettings.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.MusicLoverSettings.Add(generalSettings);
|
_context.GeneralSettings.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.MusicLoverSettings.SingleAsync(m => m.UserId == id);
|
MusicLoverSettings generalSettings = await _context.GeneralSettings.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.MusicLoverSettings.SingleAsync(m => m.UserId == id);
|
MusicLoverSettings generalSettings = await _context.GeneralSettings.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.MusicLoverSettings.SingleAsync(m => m.UserId == id);
|
MusicLoverSettings generalSettings = await _context.GeneralSettings.SingleAsync(m => m.UserId == id);
|
||||||
_context.MusicLoverSettings.Remove(generalSettings);
|
_context.GeneralSettings.Remove(generalSettings);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ 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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.Regularization)
|
.Include(x => x.Regularisation)
|
||||||
.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,7 +82,7 @@ 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.Regularization = paymentInfo.DbContent;
|
command.Regularisation = paymentInfo.DbContent;
|
||||||
command.PaymentId = token;
|
command.PaymentId = token;
|
||||||
bool paymentOk = false;
|
bool paymentOk = false;
|
||||||
if (paymentInfo.DetailsFromPayPal != null)
|
if (paymentInfo.DetailsFromPayPal != 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.Regularization)
|
.Include(x => x.Regularisation)
|
||||||
.SingleOrDefaultAsync(m => m.Id == id);
|
.SingleOrDefaultAsync(m => m.Id == id);
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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.Regularization).Include(p => p.Repository);
|
var applicationDbContext = _context.Project.Include(p => p.Client).Include(p => p.Context).Include(p => p.PerformerProfile).Include(p => p.Regularisation).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.1.0" />
|
<PackageVersion Include="AsciiDocSharp" Version="0.2.0" />
|
||||||
<PackageVersion Include="AsciiDocSharp.Converters.Html" Version="0.1.0" />
|
<PackageVersion Include="AsciiDocSharp.Converters.Html" Version="0.2.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.1.0-alpha.171" />
|
<PackageVersion Include="HigginsSoft.IdentityServer8.AspNetIdentity" Version="8.0.5-preview-net9" />
|
||||||
<PackageVersion Include="HigginsSoft.IdentityServer8.EntityFramework.Storage" Version="8.1.0-alpha.171" />
|
<PackageVersion Include="HigginsSoft.IdentityServer8.EntityFramework.Storage" Version="8.0.5-preview-net9" />
|
||||||
<PackageVersion Include="HigginsSoft.IdentityServer8.Security" Version="8.1.0-alpha.171" />
|
<PackageVersion Include="HigginsSoft.IdentityServer8.Security" Version="8.0.5-preview-net9" />
|
||||||
<PackageVersion Include="HigginsSoft.IdentityServer8.Storage" Version="8.1.0-alpha.171" />
|
<PackageVersion Include="HigginsSoft.IdentityServer8.Storage" Version="8.0.5-preview-net9" />
|
||||||
<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,5 +1,8 @@
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,6 @@ using Microsoft.AspNetCore.Localization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor;
|
using Microsoft.AspNetCore.Mvc.Razor;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.Extensions.FileProviders;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
@ -44,7 +41,6 @@ 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;
|
||||||
|
|
||||||
|
|
@ -53,30 +49,6 @@ public static class HostingExtensions
|
||||||
{
|
{
|
||||||
private const string InMemoryProviderName = "InMemory";
|
private const string InMemoryProviderName = "InMemory";
|
||||||
|
|
||||||
private static void IgnoreKnownFalsePositiveMigrationWarnings(DbContextOptionsBuilder options)
|
|
||||||
{
|
|
||||||
options.ConfigureWarnings(w =>
|
|
||||||
w.Ignore(RelationalEventId.PendingModelChangesWarning));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task ApplyMigrationsAsync<TContext>(IServiceProvider services, ILogger logger)
|
|
||||||
where TContext : DbContext
|
|
||||||
{
|
|
||||||
var contextName = typeof(TContext).Name;
|
|
||||||
var db = services.GetRequiredService<TContext>();
|
|
||||||
|
|
||||||
logger.LogInformation(
|
|
||||||
"Applying database migrations for {DbContext} using provider {Provider}...",
|
|
||||||
contextName,
|
|
||||||
db.Database.ProviderName ?? "(null)");
|
|
||||||
|
|
||||||
await db.Database.MigrateAsync();
|
|
||||||
|
|
||||||
logger.LogInformation(
|
|
||||||
"Database migrations applied successfully for {DbContext}.",
|
|
||||||
contextName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static WebApplication ConfigureWebAppServices(this WebApplicationBuilder builder)
|
public static WebApplication ConfigureWebAppServices(this WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
@ -182,13 +154,6 @@ public static class HostingExtensions
|
||||||
{
|
{
|
||||||
options.UseNpgsql(connectionString,
|
options.UseNpgsql(connectionString,
|
||||||
options => options.MigrationsAssembly(typeof(Program).Assembly));
|
options => options.MigrationsAssembly(typeof(Program).Assembly));
|
||||||
|
|
||||||
// EF Core 10 can raise PendingModelChangesWarning at runtime
|
|
||||||
// even when the snapshot and generated migrations are already
|
|
||||||
// aligned on this codebase. Treat that known false positive as
|
|
||||||
// non-fatal in every environment so production startup matches
|
|
||||||
// the behavior already observed in development.
|
|
||||||
IgnoreKnownFalsePositiveMigrationWarnings(options);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -351,7 +316,6 @@ public static class HostingExtensions
|
||||||
{
|
{
|
||||||
b.UseNpgsql(connectionString,
|
b.UseNpgsql(connectionString,
|
||||||
sql => sql.MigrationsAssembly(migrationsAssembly));
|
sql => sql.MigrationsAssembly(migrationsAssembly));
|
||||||
IgnoreKnownFalsePositiveMigrationWarnings(b);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: don't b.UseSeeding(...) here — EF Core's UseSeeding
|
// NOTE: don't b.UseSeeding(...) here — EF Core's UseSeeding
|
||||||
|
|
@ -375,7 +339,6 @@ public static class HostingExtensions
|
||||||
{
|
{
|
||||||
b.UseNpgsql(connectionString,
|
b.UseNpgsql(connectionString,
|
||||||
sql => sql.MigrationsAssembly(migrationsAssembly));
|
sql => sql.MigrationsAssembly(migrationsAssembly));
|
||||||
IgnoreKnownFalsePositiveMigrationWarnings(b);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -926,13 +889,11 @@ public static class HostingExtensions
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
await app.MigrateDatabaseAsync();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
app.UseExceptionHandler("/Home/Error");
|
app.UseExceptionHandler("/Home/Error");
|
||||||
logger.LogInformation("Running in production mode. Ensure the database is migrated.");
|
app.MigrateDatabase();
|
||||||
await app.MigrateDatabaseAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Use(async (context, next) =>
|
app.Use(async (context, next) =>
|
||||||
|
|
@ -979,37 +940,26 @@ public static class HostingExtensions
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task MigrateDatabaseAsync(this IApplicationBuilder app)
|
private static void MigrateDatabase(this IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
|
using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
|
||||||
{
|
{
|
||||||
var logger = serviceScope.ServiceProvider
|
|
||||||
.GetRequiredService<ILoggerFactory>()
|
|
||||||
.CreateLogger("Yavsc.Org.Migrations");
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var scope = app.ApplicationServices.CreateScope())
|
foreach (Type contextType in new Type[]
|
||||||
{
|
{
|
||||||
await ApplyMigrationsAsync<ApplicationDbContext>(scope.ServiceProvider, logger);
|
typeof(ApplicationDbContext)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
((DbContext)serviceScope.ServiceProvider
|
||||||
|
.GetRequiredService(contextType))
|
||||||
|
.Database.Migrate();
|
||||||
}
|
}
|
||||||
EnsureCriticalSchema(serviceScope.ServiceProvider, logger);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (InvalidOperationException ex)
|
||||||
{
|
{
|
||||||
app.Properties["DegradedDBContext"] = ex.Message;
|
app.Properties["DegradedDBContext"] = ex.Message;
|
||||||
logger.LogError(
|
|
||||||
ex,
|
|
||||||
"Database migration failed for {DbContext}. App started in degraded mode.",
|
|
||||||
nameof(ApplicationDbContext));
|
|
||||||
|
|
||||||
// EF Core 10 may raise PendingModelChangesWarning as an exception.
|
|
||||||
// Dump a concise model diff to make the mismatch actionable.
|
|
||||||
if (ex is InvalidOperationException ioe
|
|
||||||
&& ioe.Message.Contains("PendingModelChangesWarning", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
LogPendingModelChanges(serviceScope.ServiceProvider, logger);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1022,88 +972,6 @@ public static class HostingExtensions
|
||||||
SeedConfigurationDatabase(app);
|
SeedConfigurationDatabase(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void LogPendingModelChanges(IServiceProvider services, ILogger logger)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var scope = services.CreateScope();
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
||||||
var migrationsAssembly = db.GetService<IMigrationsAssembly>();
|
|
||||||
var snapshotModel = migrationsAssembly.ModelSnapshot?.Model;
|
|
||||||
if (snapshotModel is null)
|
|
||||||
{
|
|
||||||
logger.LogWarning("Pending-model diagnostic: no ModelSnapshot found for ApplicationDbContext.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogWarning(
|
|
||||||
"Pending-model diagnostic: provider={Provider}",
|
|
||||||
db.Database.ProviderName ?? "(null)");
|
|
||||||
|
|
||||||
var runtimeDeclDate = db.Model
|
|
||||||
.FindEntityType("Yavsc.Models.Identity.DeviceDeclaration")?
|
|
||||||
.FindProperty("DeclarationDate")?
|
|
||||||
.GetDefaultValueSql();
|
|
||||||
|
|
||||||
var snapshotDeclDate = snapshotModel
|
|
||||||
.FindEntityType("Yavsc.Models.Identity.DeviceDeclaration")?
|
|
||||||
.FindProperty("DeclarationDate")?
|
|
||||||
.GetDefaultValueSql();
|
|
||||||
|
|
||||||
logger.LogWarning(
|
|
||||||
"Pending-model diagnostic: DeviceDeclaration.DeclarationDate default SQL runtime='{RuntimeDefaultSql}', snapshot='{SnapshotDefaultSql}'.",
|
|
||||||
runtimeDeclDate ?? "(null)",
|
|
||||||
snapshotDeclDate ?? "(null)");
|
|
||||||
|
|
||||||
bool runtimeHasMusicLoverSettings = db.Model.FindEntityType("Yavsc.Models.Musical.Profiles.MusicLoverSettings") is not null;
|
|
||||||
bool snapshotHasMusicLoverSettings = snapshotModel.FindEntityType("Yavsc.Models.Musical.Profiles.MusicLoverSettings") is not null;
|
|
||||||
logger.LogWarning(
|
|
||||||
"Pending-model diagnostic: MusicLoverSettings runtime={RuntimeHas} snapshot={SnapshotHas}.",
|
|
||||||
runtimeHasMusicLoverSettings,
|
|
||||||
snapshotHasMusicLoverSettings);
|
|
||||||
|
|
||||||
bool runtimeHasSignature = db.Model.FindEntityType("Yavsc.Models.Billing.Signature") is not null;
|
|
||||||
bool snapshotHasSignature = snapshotModel.FindEntityType("Yavsc.Models.Billing.Signature") is not null;
|
|
||||||
logger.LogWarning(
|
|
||||||
"Pending-model diagnostic: Signature runtime={RuntimeHas} snapshot={SnapshotHas}.",
|
|
||||||
runtimeHasSignature,
|
|
||||||
snapshotHasSignature);
|
|
||||||
|
|
||||||
bool runtimeHasModerated = db.Model
|
|
||||||
.FindEntityType("Yavsc.Models.Workflow.Activity")?
|
|
||||||
.FindProperty("Moderated") is not null;
|
|
||||||
bool snapshotHasModerated = snapshotModel
|
|
||||||
.FindEntityType("Yavsc.Models.Workflow.Activity")?
|
|
||||||
.FindProperty("Moderated") is not null;
|
|
||||||
logger.LogWarning(
|
|
||||||
"Pending-model diagnostic: Activity.Moderated runtime={RuntimeHas} snapshot={SnapshotHas}.",
|
|
||||||
runtimeHasModerated,
|
|
||||||
snapshotHasModerated);
|
|
||||||
}
|
|
||||||
catch (Exception diagEx)
|
|
||||||
{
|
|
||||||
logger.LogError(diagEx, "Pending-model diagnostic failed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void EnsureCriticalSchema(IServiceProvider services, ILogger logger)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var db = services.GetRequiredService<ApplicationDbContext>();
|
|
||||||
|
|
||||||
// Hotfix guard: keep startup resilient if a migration was skipped,
|
|
||||||
// while still allowing EF migrations to be the source of truth.
|
|
||||||
db.Database.ExecuteSqlRaw(@"
|
|
||||||
ALTER TABLE ""Activities""
|
|
||||||
ADD COLUMN IF NOT EXISTS ""Moderated"" boolean NOT NULL DEFAULT FALSE;");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Critical schema check failed for Activities.Moderated.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SeedConfigurationDatabase(IApplicationBuilder app)
|
private static void SeedConfigurationDatabase(IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -1112,26 +980,13 @@ ADD COLUMN IF NOT EXISTS ""Moderated"" boolean NOT NULL DEFAULT FALSE;");
|
||||||
.GetRequiredService<IServiceScopeFactory>()
|
.GetRequiredService<IServiceScopeFactory>()
|
||||||
.CreateScope();
|
.CreateScope();
|
||||||
|
|
||||||
var logger = app.ApplicationServices
|
|
||||||
.GetRequiredService<ILoggerFactory>()
|
|
||||||
.CreateLogger("Yavsc.Org.Seeding");
|
|
||||||
|
|
||||||
var configurationDb = scope.ServiceProvider
|
var configurationDb = scope.ServiceProvider
|
||||||
.GetRequiredService<IdentityServer8.EntityFramework.DbContexts.ConfigurationDbContext>();
|
.GetRequiredService<IdentityServer8.EntityFramework.DbContexts.ConfigurationDbContext>();
|
||||||
|
|
||||||
var configuration = scope.ServiceProvider
|
var configuration = scope.ServiceProvider
|
||||||
.GetRequiredService<IConfiguration>();
|
.GetRequiredService<IConfiguration>();
|
||||||
|
|
||||||
logger.LogInformation(
|
|
||||||
"Running seed for {DbContext} using provider {Provider}...",
|
|
||||||
nameof(IdentityServer8.EntityFramework.DbContexts.ConfigurationDbContext),
|
|
||||||
configurationDb.Database.ProviderName ?? "(null)");
|
|
||||||
|
|
||||||
EnsureDefaultConfiguration(configuration)(configurationDb, true);
|
EnsureDefaultConfiguration(configuration)(configurationDb, true);
|
||||||
|
|
||||||
logger.LogInformation(
|
|
||||||
"Seed completed for {DbContext}.",
|
|
||||||
nameof(IdentityServer8.EntityFramework.DbContexts.ConfigurationDbContext));
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -1141,10 +996,7 @@ ADD COLUMN IF NOT EXISTS ""Moderated"" boolean NOT NULL DEFAULT FALSE;");
|
||||||
var logger = app.ApplicationServices
|
var logger = app.ApplicationServices
|
||||||
.GetRequiredService<ILoggerFactory>()
|
.GetRequiredService<ILoggerFactory>()
|
||||||
.CreateLogger("Yavsc.Org.Seeding");
|
.CreateLogger("Yavsc.Org.Seeding");
|
||||||
logger.LogError(
|
logger.LogError(ex, "ConfigurationDb seeding failed.");
|
||||||
ex,
|
|
||||||
"Seed failed for {DbContext}.",
|
|
||||||
nameof(IdentityServer8.EntityFramework.DbContexts.ConfigurationDbContext));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.Provisional,
|
Previsional = query.Previsional,
|
||||||
EventDate = query.EventDate,
|
EventDate = query.EventDate,
|
||||||
Location = query.Location,
|
Location = query.Location,
|
||||||
Id = query.Id,
|
Id = query.Id,
|
||||||
|
|
@ -59,11 +59,11 @@ namespace Yavsc.Helpers
|
||||||
{
|
{
|
||||||
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.Provisional,
|
Previsional = query.Previsional,
|
||||||
EventDate = query.EventDate,
|
EventDate = query.EventDate,
|
||||||
Location = query.Location,
|
Location = query.Location,
|
||||||
Id = query.Id,
|
Id = query.Id,
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,8 @@ 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();
|
|
||||||
|
|
||||||
var systemActivities = _dbContext.Activities.Where(a=>!a.Moderated
|
List<SelectListItem> items = _dbContext.Activities.Select(
|
||||||
&& 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();
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,294 +0,0 @@
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Yavsc.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class activityModerated : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_MusicalPreference_GeneralSettings_GeneralSettingsUserId",
|
|
||||||
table: "MusicalPreference");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "GeneralSettings");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Accepted",
|
|
||||||
table: "RdvQueries");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Decided",
|
|
||||||
table: "RdvQueries");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Accepted",
|
|
||||||
table: "Project");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Decided",
|
|
||||||
table: "Project");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Accepted",
|
|
||||||
table: "HairMultiCutQueries");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Decided",
|
|
||||||
table: "HairMultiCutQueries");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Accepted",
|
|
||||||
table: "HairCutQueries");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Decided",
|
|
||||||
table: "HairCutQueries");
|
|
||||||
|
|
||||||
migrationBuilder.RenameColumn(
|
|
||||||
name: "Previsional",
|
|
||||||
table: "RdvQueries",
|
|
||||||
newName: "Provisional");
|
|
||||||
|
|
||||||
migrationBuilder.RenameColumn(
|
|
||||||
name: "Previsional",
|
|
||||||
table: "Project",
|
|
||||||
newName: "Provisional");
|
|
||||||
|
|
||||||
migrationBuilder.RenameColumn(
|
|
||||||
name: "GeneralSettingsUserId",
|
|
||||||
table: "MusicalPreference",
|
|
||||||
newName: "MusicLoverSettingsUserId");
|
|
||||||
|
|
||||||
migrationBuilder.RenameIndex(
|
|
||||||
name: "IX_MusicalPreference_GeneralSettingsUserId",
|
|
||||||
table: "MusicalPreference",
|
|
||||||
newName: "IX_MusicalPreference_MusicLoverSettingsUserId");
|
|
||||||
|
|
||||||
migrationBuilder.RenameColumn(
|
|
||||||
name: "Previsional",
|
|
||||||
table: "HairMultiCutQueries",
|
|
||||||
newName: "Provisional");
|
|
||||||
|
|
||||||
migrationBuilder.RenameColumn(
|
|
||||||
name: "Previsional",
|
|
||||||
table: "HairCutQueries",
|
|
||||||
newName: "Provisional");
|
|
||||||
|
|
||||||
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: "MusicLoverSettings",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
UserId = table.Column<string>(type: "text", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_MusicLoverSettings", x => x.UserId);
|
|
||||||
});
|
|
||||||
|
|
||||||
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_MusicLoverSettings_MusicLoverSettingsUser~",
|
|
||||||
table: "MusicalPreference",
|
|
||||||
column: "MusicLoverSettingsUserId",
|
|
||||||
principalTable: "MusicLoverSettings",
|
|
||||||
principalColumn: "UserId");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_MusicalPreference_MusicLoverSettings_MusicLoverSettingsUser~",
|
|
||||||
table: "MusicalPreference");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "MusicLoverSettings");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Signatures");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Moderated",
|
|
||||||
table: "Activities");
|
|
||||||
|
|
||||||
migrationBuilder.RenameColumn(
|
|
||||||
name: "Provisional",
|
|
||||||
table: "RdvQueries",
|
|
||||||
newName: "Previsional");
|
|
||||||
|
|
||||||
migrationBuilder.RenameColumn(
|
|
||||||
name: "Provisional",
|
|
||||||
table: "Project",
|
|
||||||
newName: "Previsional");
|
|
||||||
|
|
||||||
migrationBuilder.RenameColumn(
|
|
||||||
name: "MusicLoverSettingsUserId",
|
|
||||||
table: "MusicalPreference",
|
|
||||||
newName: "GeneralSettingsUserId");
|
|
||||||
|
|
||||||
migrationBuilder.RenameIndex(
|
|
||||||
name: "IX_MusicalPreference_MusicLoverSettingsUserId",
|
|
||||||
table: "MusicalPreference",
|
|
||||||
newName: "IX_MusicalPreference_GeneralSettingsUserId");
|
|
||||||
|
|
||||||
migrationBuilder.RenameColumn(
|
|
||||||
name: "Provisional",
|
|
||||||
table: "HairMultiCutQueries",
|
|
||||||
newName: "Previsional");
|
|
||||||
|
|
||||||
migrationBuilder.RenameColumn(
|
|
||||||
name: "Provisional",
|
|
||||||
table: "HairCutQueries",
|
|
||||||
newName: "Previsional");
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<bool>(
|
|
||||||
name: "Accepted",
|
|
||||||
table: "RdvQueries",
|
|
||||||
type: "boolean",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: false);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<bool>(
|
|
||||||
name: "Decided",
|
|
||||||
table: "RdvQueries",
|
|
||||||
type: "boolean",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: false);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<bool>(
|
|
||||||
name: "Accepted",
|
|
||||||
table: "Project",
|
|
||||||
type: "boolean",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: false);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<bool>(
|
|
||||||
name: "Decided",
|
|
||||||
table: "Project",
|
|
||||||
type: "boolean",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: false);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<bool>(
|
|
||||||
name: "Accepted",
|
|
||||||
table: "HairMultiCutQueries",
|
|
||||||
type: "boolean",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: false);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<bool>(
|
|
||||||
name: "Decided",
|
|
||||||
table: "HairMultiCutQueries",
|
|
||||||
type: "boolean",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: false);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<bool>(
|
|
||||||
name: "Accepted",
|
|
||||||
table: "HairCutQueries",
|
|
||||||
type: "boolean",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: false);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<bool>(
|
|
||||||
name: "Decided",
|
|
||||||
table: "HairCutQueries",
|
|
||||||
type: "boolean",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: false);
|
|
||||||
|
|
||||||
migrationBuilder.AlterColumn<string>(
|
|
||||||
name: "Description",
|
|
||||||
table: "Bug",
|
|
||||||
type: "text",
|
|
||||||
nullable: true,
|
|
||||||
oldClrType: typeof(string),
|
|
||||||
oldType: "character varying(10240)",
|
|
||||||
oldMaxLength: 10240,
|
|
||||||
oldNullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "GeneralSettings",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
UserId = table.Column<string>(type: "text", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_GeneralSettings", x => x.UserId);
|
|
||||||
});
|
|
||||||
|
|
||||||
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.9")
|
.HasAnnotation("ProductVersion", "10.0.8")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
@ -1413,50 +1413,6 @@ 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")
|
||||||
|
|
@ -1918,6 +1874,9 @@ namespace Yavsc.Migrations
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("Accepted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("ActivityCode")
|
b.Property<string>("ActivityCode")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
@ -1938,6 +1897,9 @@ namespace Yavsc.Migrations
|
||||||
b.Property<DateTime>("DateModified")
|
b.Property<DateTime>("DateModified")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("Decided")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
|
@ -1957,7 +1919,7 @@ namespace Yavsc.Migrations
|
||||||
b.Property<long>("PrestationId")
|
b.Property<long>("PrestationId")
|
||||||
.HasColumnType("bigint");
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
b.Property<decimal?>("Provisional")
|
b.Property<decimal?>("Previsional")
|
||||||
.HasColumnType("numeric");
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
b.Property<string>("SelectedProfileUserId")
|
b.Property<string>("SelectedProfileUserId")
|
||||||
|
|
@ -2002,6 +1964,9 @@ namespace Yavsc.Migrations
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("Accepted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("ActivityCode")
|
b.Property<string>("ActivityCode")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
@ -2019,6 +1984,9 @@ namespace Yavsc.Migrations
|
||||||
b.Property<DateTime>("DateModified")
|
b.Property<DateTime>("DateModified")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("Decided")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
|
@ -2035,7 +2003,7 @@ namespace Yavsc.Migrations
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<decimal?>("Provisional")
|
b.Property<decimal?>("Previsional")
|
||||||
.HasColumnType("numeric");
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
b.Property<int>("Status")
|
b.Property<int>("Status")
|
||||||
|
|
@ -2189,8 +2157,7 @@ namespace Yavsc.Migrations
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
.HasMaxLength(10240)
|
.HasColumnType("text");
|
||||||
.HasColumnType("character varying(10240)");
|
|
||||||
|
|
||||||
b.Property<long?>("FeatureId")
|
b.Property<long?>("FeatureId")
|
||||||
.HasColumnType("bigint");
|
.HasColumnType("bigint");
|
||||||
|
|
@ -2567,7 +2534,7 @@ namespace Yavsc.Migrations
|
||||||
b.Property<string>("DjSettingsUserId")
|
b.Property<string>("DjSettingsUserId")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("MusicLoverSettingsUserId")
|
b.Property<string>("GeneralSettingsUserId")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<int>("Rate")
|
b.Property<int>("Rate")
|
||||||
|
|
@ -2580,7 +2547,7 @@ namespace Yavsc.Migrations
|
||||||
|
|
||||||
b.HasIndex("DjSettingsUserId");
|
b.HasIndex("DjSettingsUserId");
|
||||||
|
|
||||||
b.HasIndex("MusicLoverSettingsUserId");
|
b.HasIndex("GeneralSettingsUserId");
|
||||||
|
|
||||||
b.HasIndex("TendencyId");
|
b.HasIndex("TendencyId");
|
||||||
|
|
||||||
|
|
@ -2618,6 +2585,16 @@ 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")
|
||||||
|
|
@ -2633,16 +2610,6 @@ 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("MusicLoverSettings");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b =>
|
modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("CreationToken")
|
b.Property<string>("CreationToken")
|
||||||
|
|
@ -2927,9 +2894,6 @@ 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");
|
||||||
|
|
||||||
|
|
@ -3079,6 +3043,9 @@ namespace Yavsc.Migrations
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("Accepted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("ActivityCode")
|
b.Property<string>("ActivityCode")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
@ -3096,6 +3063,9 @@ namespace Yavsc.Migrations
|
||||||
b.Property<DateTime>("DateModified")
|
b.Property<DateTime>("DateModified")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("Decided")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
|
@ -3115,7 +3085,7 @@ namespace Yavsc.Migrations
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<decimal?>("Provisional")
|
b.Property<decimal?>("Previsional")
|
||||||
.HasColumnType("numeric");
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
b.Property<string>("Reason")
|
b.Property<string>("Reason")
|
||||||
|
|
@ -3222,6 +3192,9 @@ namespace Yavsc.Migrations
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("Accepted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("ActivityCode")
|
b.Property<string>("ActivityCode")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
@ -3239,6 +3212,9 @@ namespace Yavsc.Migrations
|
||||||
b.Property<DateTime>("DateModified")
|
b.Property<DateTime>("DateModified")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("Decided")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
|
@ -3259,7 +3235,7 @@ namespace Yavsc.Migrations
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<decimal?>("Provisional")
|
b.Property<decimal?>("Previsional")
|
||||||
.HasColumnType("numeric");
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
b.Property<int>("Status")
|
b.Property<int>("Status")
|
||||||
|
|
@ -3761,25 +3737,6 @@ 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")
|
||||||
|
|
@ -3961,7 +3918,7 @@ namespace Yavsc.Migrations
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("LocationId");
|
.HasForeignKey("LocationId");
|
||||||
|
|
||||||
b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization")
|
b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularisation")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("PaymentId");
|
.HasForeignKey("PaymentId");
|
||||||
|
|
||||||
|
|
@ -3991,7 +3948,7 @@ namespace Yavsc.Migrations
|
||||||
|
|
||||||
b.Navigation("Prestation");
|
b.Navigation("Prestation");
|
||||||
|
|
||||||
b.Navigation("Regularization");
|
b.Navigation("Regularisation");
|
||||||
|
|
||||||
b.Navigation("SelectedProfile");
|
b.Navigation("SelectedProfile");
|
||||||
});
|
});
|
||||||
|
|
@ -4014,7 +3971,7 @@ namespace Yavsc.Migrations
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("LocationId");
|
.HasForeignKey("LocationId");
|
||||||
|
|
||||||
b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization")
|
b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularisation")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("PaymentId");
|
.HasForeignKey("PaymentId");
|
||||||
|
|
||||||
|
|
@ -4032,7 +3989,7 @@ namespace Yavsc.Migrations
|
||||||
|
|
||||||
b.Navigation("PerformerProfile");
|
b.Navigation("PerformerProfile");
|
||||||
|
|
||||||
b.Navigation("Regularization");
|
b.Navigation("Regularisation");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b =>
|
modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b =>
|
||||||
|
|
@ -4205,9 +4162,9 @@ namespace Yavsc.Migrations
|
||||||
.WithMany("SoundColor")
|
.WithMany("SoundColor")
|
||||||
.HasForeignKey("DjSettingsUserId");
|
.HasForeignKey("DjSettingsUserId");
|
||||||
|
|
||||||
b.HasOne("Yavsc.Models.Musical.Profiles.MusicLoverSettings", null)
|
b.HasOne("Yavsc.Models.Musical.Profiles.GeneralSettings", null)
|
||||||
.WithMany("SoundColor")
|
.WithMany("SoundColor")
|
||||||
.HasForeignKey("MusicLoverSettingsUserId");
|
.HasForeignKey("GeneralSettingsUserId");
|
||||||
|
|
||||||
b.HasOne("Yavsc.Models.Musical.MusicalTendency", "MusicalTendency")
|
b.HasOne("Yavsc.Models.Musical.MusicalTendency", "MusicalTendency")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
|
|
@ -4387,7 +4344,7 @@ namespace Yavsc.Migrations
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("LocationId");
|
.HasForeignKey("LocationId");
|
||||||
|
|
||||||
b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization")
|
b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularisation")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("PaymentId");
|
.HasForeignKey("PaymentId");
|
||||||
|
|
||||||
|
|
@ -4405,7 +4362,7 @@ namespace Yavsc.Migrations
|
||||||
|
|
||||||
b.Navigation("PerformerProfile");
|
b.Navigation("PerformerProfile");
|
||||||
|
|
||||||
b.Navigation("Regularization");
|
b.Navigation("Regularisation");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b =>
|
modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b =>
|
||||||
|
|
@ -4447,7 +4404,7 @@ namespace Yavsc.Migrations
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization")
|
b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularisation")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("PaymentId");
|
.HasForeignKey("PaymentId");
|
||||||
|
|
||||||
|
|
@ -4463,7 +4420,7 @@ namespace Yavsc.Migrations
|
||||||
|
|
||||||
b.Navigation("PerformerProfile");
|
b.Navigation("PerformerProfile");
|
||||||
|
|
||||||
b.Navigation("Regularization");
|
b.Navigation("Regularisation");
|
||||||
|
|
||||||
b.Navigation("Repository");
|
b.Navigation("Repository");
|
||||||
});
|
});
|
||||||
|
|
@ -4562,8 +4519,6 @@ 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 =>
|
||||||
|
|
@ -4625,7 +4580,7 @@ namespace Yavsc.Migrations
|
||||||
b.Navigation("SoundColor");
|
b.Navigation("SoundColor");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b =>
|
modelBuilder.Entity("Yavsc.Models.Musical.Profiles.GeneralSettings", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("SoundColor");
|
b.Navigation("SoundColor");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
<?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>
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
<?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>
|
|
||||||
|
|
@ -101,7 +101,7 @@ namespace Yavsc.Services
|
||||||
return _isCop[userName];
|
return _isCop[userName];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnDisconnected(string connectionId)
|
public void OnDisctonnected(string connectionId)
|
||||||
{
|
{
|
||||||
string uname;
|
string uname;
|
||||||
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
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;
|
||||||
|
|
@ -7,7 +8,6 @@ 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
|
||||||
{
|
{
|
||||||
|
|
@ -98,19 +98,19 @@ namespace Yavsc.Services
|
||||||
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,9 +152,22 @@ namespace Yavsc.Services
|
||||||
return await NotifyEvent<HairCutQueryEvent>(userIds, ev);
|
return await NotifyEvent<HairCutQueryEvent>(userIds, ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MessageWithPayloadResponse> NotifyAsync(IEnumerable<string> userIds, IEvent yavscEvent)
|
public async Task<MessageWithPayloadResponse> NotifyAsync(IEnumerable<string> userIds, IEvent yaev)
|
||||||
{
|
{
|
||||||
return await NotifyEvent<IEvent>(userIds, yavscEvent);
|
return await NotifyEvent<IEvent>(userIds, yaev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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,8 +1,4 @@
|
||||||
@{
|
|
||||||
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,7 +1,4 @@
|
||||||
@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">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
@model string
|
@model string
|
||||||
|
|
||||||
@{
|
|
||||||
ViewBag.Title = Localizer["Account"];
|
|
||||||
}
|
|
||||||
|
|
||||||
<h1>Check your mail box!</h1>
|
<h1>Check your mail box!</h1>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
@{
|
@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,7 +1,4 @@
|
||||||
@model LoginViewModel
|
@model LoginViewModel
|
||||||
@{
|
|
||||||
ViewBag.Title = Localizer["Account"];
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="login-page">
|
<div class="login-page">
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
@{
|
@model LogoutViewModel
|
||||||
ViewBag.Title = Localizer["Account"];
|
|
||||||
}
|
|
||||||
|
|
||||||
@model LogoutViewModel
|
|
||||||
|
|
||||||
<div class="logout-page">
|
<div class="logout-page">
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
@model RegisterModel
|
@model RegisterModel
|
||||||
@{
|
|
||||||
ViewBag.Title = Localizer["Account"];
|
|
||||||
}
|
|
||||||
|
|
||||||
<partial name="_ValidationSummary" />
|
<partial name="_ValidationSummary" />
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
@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>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
@model SignInModel
|
@model SignInModel
|
||||||
@{
|
|
||||||
ViewBag.Title = Localizer["Account"];
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="login-page">
|
<div class="login-page">
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
@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,19 +21,7 @@
|
||||||
@foreach (var user in Model.Users) {
|
@foreach (var user in Model.Users) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@if (user.UserId==User.GetUserId()) {
|
<img src="~/avatars/@(user.UserName).xs.png" alt="avatar"/>
|
||||||
<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,9 +1,20 @@
|
||||||
@model IdentityServer8.EntityFramework.Entities.ApiScope
|
@model IdentityServer8.EntityFramework.Entities.ApiScope
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewBag.Title = Localizer["ApiScope"];
|
Layout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!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">
|
||||||
|
|
@ -53,3 +64,6 @@
|
||||||
<div>
|
<div>
|
||||||
<a asp-action="Index">Back to List</a>
|
<a asp-action="Index">Back to List</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,18 @@
|
||||||
@model Yavsc.Models.YavscApiScope
|
@model Yavsc.Models.YavscApiScope
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewBag.Title = Localizer["ApiScope"];
|
Layout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!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>
|
||||||
|
|
@ -57,3 +67,6 @@
|
||||||
<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,8 +1,17 @@
|
||||||
@model Yavsc.Models.YavscApiScope
|
@model Yavsc.Models.YavscApiScope
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewBag.Title = Localizer["ApiScope"];
|
Layout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<title>Details</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4>YavscApiScope</h4>
|
<h4>YavscApiScope</h4>
|
||||||
|
|
@ -56,3 +65,5 @@
|
||||||
<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,8 +1,17 @@
|
||||||
@model Yavsc.Models.YavscApiScope
|
@model Yavsc.Models.YavscApiScope
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewBag.Title = Localizer["ApiScope"];
|
Layout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<title>Edit</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
<h4>YavscApiScope</h4>
|
<h4>YavscApiScope</h4>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
@ -56,3 +65,6 @@
|
||||||
<div>
|
<div>
|
||||||
<a asp-action="Index">Back to List</a>
|
<a asp-action="Index">Back to List</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,17 @@
|
||||||
@model IEnumerable<Yavsc.Models.YavscApiScope>
|
@model IEnumerable<Yavsc.Models.YavscApiScope>
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewBag.Title = Localizer["ApiScope"];
|
Layout = null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!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>
|
||||||
|
|
@ -68,3 +75,5 @@
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
@model Client
|
@model Client
|
||||||
@{
|
|
||||||
ViewBag.Title = Localizer["Client"];
|
|
||||||
}
|
|
||||||
|
|
||||||
<h2>@Localizer["Create"]</h2>
|
<h2>@Localizer["Create"]</h2>
|
||||||
|
|
||||||
|
|
@ -80,3 +77,4 @@
|
||||||
<div>
|
<div>
|
||||||
<a asp-action="Index">@Localizer["Back to List"]</a>
|
<a asp-action="Index">@Localizer["Back to List"]</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
@model Client
|
@model Client
|
||||||
@{
|
|
||||||
ViewBag.Title = Localizer["Client"];
|
|
||||||
}
|
|
||||||
|
|
||||||
<h2>@Localizer["Delete"]</h2>
|
<h2>@Localizer["Delete"]</h2>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,7 @@
|
||||||
@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,7 +1,4 @@
|
||||||
@model Client
|
@model Client
|
||||||
@{
|
|
||||||
ViewBag.Title = Localizer["Client"];
|
|
||||||
}
|
|
||||||
|
|
||||||
<h2>@Localizer["Edit"]</h2>
|
<h2>@Localizer["Edit"]</h2>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
@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;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
@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";
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
@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";
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
@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";
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
@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";
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
@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;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
@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";
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
@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";
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
@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;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
@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>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
@model Client
|
@model Client
|
||||||
@{
|
|
||||||
ViewBag.Title = Localizer["Client"];
|
|
||||||
}
|
|
||||||
|
|
||||||
<h2>@Localizer["Regenerate client secret"]</h2>
|
<h2>@Localizer["Regenerate client secret"]</h2>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
@{
|
|
||||||
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 = Localizer["CommandForms"];
|
ViewBag.Title = "Index";
|
||||||
}
|
}
|
||||||
|
|
||||||
<h2>@ViewBag.Title</h2>
|
<h2>Index</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a asp-action="Create">Create New</a>
|
<a asp-action="Create">Create New</a>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
@{
|
|
||||||
ViewBag.Title = Localizer["Device"];
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="page-device-success">
|
<div class="page-device-success">
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
@model string
|
@model string
|
||||||
@{
|
|
||||||
ViewBag.Title = Localizer["Device"];
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="page-device-code">
|
<div class="page-device-code">
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
@model DeviceAuthorizationViewModel
|
@model DeviceAuthorizationViewModel
|
||||||
@{
|
|
||||||
ViewBag.Title = Localizer["Device"];
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="page-device-confirmation">
|
<div class="page-device-confirmation">
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
@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>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
@model Instrumentation
|
@model Instrumentation
|
||||||
@{
|
|
||||||
ViewBag.Title = Localizer["Do"];
|
|
||||||
}
|
|
||||||
|
|
||||||
<h2>@Model.Tool.Name</h2>
|
<h2>@Model.Tool.Name</h2>
|
||||||
|
|
||||||
|
|
|
||||||
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