Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31f0ad4490 | ||
|
|
b6307fb359 | ||
|
|
d37b5a2edb |
@@ -1,5 +0,0 @@
|
||||
UMBRACO_CLIENT_ID=umbraco-back-office-mcp
|
||||
UMBRACO_CLIENT_SECRET=1234567890
|
||||
UMBRACO_BASE_URL=https://localhost:44339
|
||||
NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
UMBRACO_INCLUDE_TOOL_COLLECTIONS=data-type,document-type,document,media-type,media
|
||||
@@ -55,8 +55,3 @@
|
||||
*.sln text=auto eol=crlf merge=union
|
||||
|
||||
*.gitattributes text=auto
|
||||
|
||||
# Generated files - hidden by default in GitHub diffs
|
||||
src/Umbraco.Web.UI.Client/src/packages/core/backend-api/** linguist-generated
|
||||
src/Umbraco.Web.UI.Login/src/api/** linguist-generated
|
||||
src/Umbraco.Cms.Api.Management/OpenApi.json linguist-generated
|
||||
|
||||
@@ -1 +1,218 @@
|
||||
The full development guide for this repository lives in [CLAUDE.md](../CLAUDE.md). Please read that file for complete instructions on architecture, build steps, testing, branching conventions, and coding patterns.
|
||||
# Umbraco CMS Development Guide
|
||||
|
||||
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
|
||||
|
||||
## Working Effectively
|
||||
|
||||
Bootstrap, build, and test the repository:
|
||||
|
||||
- Install .NET SDK (version specified in global.json):
|
||||
- `curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --version $(jq -r '.sdk.version' global.json)`
|
||||
- `export PATH="/home/runner/.dotnet:$PATH"`
|
||||
- Install Node.js (version specified in src/Umbraco.Web.UI.Client/.nvmrc):
|
||||
- `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash`
|
||||
- `export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"`
|
||||
- `nvm install $(cat src/Umbraco.Web.UI.Client/.nvmrc) && nvm use $(cat src/Umbraco.Web.UI.Client/.nvmrc)`
|
||||
- Fix shallow clone issue (required for GitVersioning):
|
||||
- `git fetch --unshallow`
|
||||
- Restore packages:
|
||||
- `dotnet restore` -- takes 50 seconds. NEVER CANCEL. Set timeout to 90+ seconds.
|
||||
- Build the solution:
|
||||
- `dotnet build` -- takes 4.5 minutes. NEVER CANCEL. Set timeout to 10+ minutes.
|
||||
- Install and build frontend:
|
||||
- `cd src/Umbraco.Web.UI.Client`
|
||||
- `npm ci --no-fund --no-audit --prefer-offline` -- takes 11 seconds.
|
||||
- `npm run build:for:cms` -- takes 1.25 minutes. NEVER CANCEL. Set timeout to 5+ minutes.
|
||||
- Install and build Login
|
||||
- `cd src/Umbraco.Web.UI.Login`
|
||||
- `npm ci --no-fund --no-audit --prefer-offline`
|
||||
- `npm run build`
|
||||
- Run the application:
|
||||
- `cd src/Umbraco.Web.UI`
|
||||
- `dotnet run --no-build` -- Application runs on https://localhost:44339 and http://localhost:11000
|
||||
|
||||
Check out [BUILD.md](./BUILD.md) for more detailed instructions.
|
||||
|
||||
## Validation
|
||||
|
||||
- ALWAYS run through at least one complete end-to-end scenario after making changes.
|
||||
- Build and unit tests must pass before committing changes.
|
||||
- Frontend build produces output in src/Umbraco.Web.UI.Client/dist-cms/ which gets copied to src/Umbraco.Web.UI/wwwroot/umbraco/backoffice/
|
||||
- Always run `dotnet build` and `npm run build:for:cms` before running the application to see your changes.
|
||||
- For login-only changes, you can run `npm run build` from src/Umbraco.Web.UI.Login and then `dotnet run --no-build` from src/Umbraco.Web.UI.
|
||||
- For frontend-only changes, you can run `npm run dev:server` from src/Umbraco.Web.UI.Client for hot reloading.
|
||||
- Frontend changes should be linted using `npm run lint:fix` which uses Eslint.
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests (.NET)
|
||||
|
||||
- Location: tests/Umbraco.Tests.UnitTests/
|
||||
- Run: `dotnet test tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj --configuration Release --verbosity minimal`
|
||||
- Duration: ~1 minute with 3,343 tests
|
||||
- NEVER CANCEL: Set timeout to 5+ minutes
|
||||
|
||||
### Integration Tests (.NET)
|
||||
|
||||
- Location: tests/Umbraco.Tests.Integration/
|
||||
- Run: `dotnet test tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj --configuration Release --verbosity minimal`
|
||||
- NEVER CANCEL: Set timeout to 10+ minutes
|
||||
|
||||
### Frontend Tests
|
||||
|
||||
- Location: src/Umbraco.Web.UI.Client/
|
||||
- Run: `npm test` (requires `npx playwright install` first)
|
||||
- Frontend tests use Web Test Runner with Playwright
|
||||
|
||||
### Acceptance Tests (E2E)
|
||||
|
||||
- Location: tests/Umbraco.Tests.AcceptanceTest/
|
||||
- Requires running Umbraco application and configuration
|
||||
- See tests/Umbraco.Tests.AcceptanceTest/README.md for detailed setup (requires `npx playwright install` first)
|
||||
|
||||
## Project Structure
|
||||
|
||||
The solution contains 30 C# projects organized as follows:
|
||||
|
||||
### Main Application Projects
|
||||
|
||||
- **Umbraco.Web.UI**: Main web application project (startup project)
|
||||
- **Umbraco.Web.UI.Client**: TypeScript frontend (backoffice)
|
||||
- **Umbraco.Web.UI.Login**: Separate login screen frontend
|
||||
- **Umbraco.Core**: Core domain models and interfaces
|
||||
- **Umbraco.Infrastructure**: Data access and infrastructure
|
||||
- **Umbraco.Cms**: Main CMS package
|
||||
|
||||
### API Projects
|
||||
|
||||
- **Umbraco.Cms.Api.Management**: Management API
|
||||
- **Umbraco.Cms.Api.Delivery**: Content Delivery API
|
||||
- **Umbraco.Cms.Api.Common**: Shared API components
|
||||
|
||||
### Persistence Projects
|
||||
|
||||
- **Umbraco.Cms.Persistence.SqlServer**: SQL Server support
|
||||
- **Umbraco.Cms.Persistence.Sqlite**: SQLite support
|
||||
- **Umbraco.Cms.Persistence.EFCore**: Entity Framework Core abstractions
|
||||
|
||||
### Test Projects
|
||||
|
||||
- **Umbraco.Tests.UnitTests**: Unit tests
|
||||
- **Umbraco.Tests.Integration**: Integration tests
|
||||
- **Umbraco.Tests.AcceptanceTest**: End-to-end tests with Playwright
|
||||
- **Umbraco.Tests.Common**: Shared test utilities
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Running Umbraco in Different Modes
|
||||
|
||||
**Production Mode (Standard Development)**
|
||||
Use this for backend development, testing full builds, or when you don't need hot reloading:
|
||||
|
||||
1. Build frontend assets: `cd src/Umbraco.Web.UI.Client && npm run build:for:cms`
|
||||
2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build`
|
||||
3. Access backoffice: `https://localhost:44339/umbraco`
|
||||
4. Application uses compiled frontend from `wwwroot/umbraco/backoffice/`
|
||||
|
||||
**Vite Dev Server Mode (Frontend Development with Hot Reload)**
|
||||
Use this for frontend-only development with hot module reloading:
|
||||
|
||||
1. Configure backend for frontend development - Add to `src/Umbraco.Web.UI/appsettings.json` under `Umbraco:CMS:Security`:
|
||||
```json
|
||||
"BackOfficeHost": "http://localhost:5173",
|
||||
"AuthorizeCallbackPathName": "/oauth_complete",
|
||||
"AuthorizeCallbackLogoutPathName": "/logout",
|
||||
"AuthorizeCallbackErrorPathName": "/error",
|
||||
"BackOfficeTokenCookie": {
|
||||
"SameSite": "None"
|
||||
}
|
||||
```
|
||||
2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build`
|
||||
3. Run frontend dev server: `cd src/Umbraco.Web.UI.Client && npm run dev:server`
|
||||
4. Access backoffice: `http://localhost:5173/` (no `/umbraco` prefix)
|
||||
5. Changes to TypeScript/Lit files hot reload automatically
|
||||
|
||||
**Important:** Remove the `BackOfficeHost` configuration before committing or switching back to production mode.
|
||||
|
||||
### Backend-Only Development
|
||||
|
||||
For backend-only changes, disable frontend builds:
|
||||
|
||||
- Comment out the target named "BuildStaticAssetsPreconditions" in src/Umbraco.Cms.StaticAssets.csproj:
|
||||
```
|
||||
<!--<Target Name="BuildStaticAssetsPreconditions" BeforeTargets="AssignTargetPaths">
|
||||
[...]
|
||||
</Target>-->
|
||||
```
|
||||
- Remember to uncomment before committing
|
||||
|
||||
### Building NuGet Packages
|
||||
|
||||
To build custom NuGet packages for testing:
|
||||
|
||||
```bash
|
||||
dotnet pack -c Release -o Build.Out
|
||||
dotnet nuget add source [Path to Build.Out folder] -n MyLocalFeed
|
||||
```
|
||||
|
||||
### Regenerating Frontend API Types
|
||||
|
||||
When changing Management API:
|
||||
|
||||
```bash
|
||||
cd src/Umbraco.Web.UI.Client
|
||||
npm run generate:server-api-dev
|
||||
```
|
||||
|
||||
Also update OpenApi.json from /umbraco/swagger/management/swagger.json
|
||||
|
||||
## Database Setup
|
||||
|
||||
Default configuration supports SQLite for development. For production-like testing:
|
||||
|
||||
- Use SQL Server/LocalDb for better performance
|
||||
- Configure connection string in src/Umbraco.Web.UI/appsettings.json
|
||||
|
||||
## Clean Up / Reset
|
||||
|
||||
To reset development environment:
|
||||
|
||||
```bash
|
||||
# Remove configuration and database
|
||||
rm src/Umbraco.Web.UI/appsettings.json
|
||||
rm -rf src/Umbraco.Web.UI/umbraco/Data
|
||||
|
||||
# Full clean (removes all untracked files)
|
||||
git clean -xdf .
|
||||
```
|
||||
|
||||
## Version Information
|
||||
|
||||
- Target Framework: .NET (version specified in global.json)
|
||||
- Current Version: (specified in version.json)
|
||||
- Node.js Requirement: (specified in src/Umbraco.Web.UI.Client/.nvmrc)
|
||||
- npm Requirement: Latest compatible version
|
||||
|
||||
## Known Issues
|
||||
|
||||
- Build requires full git history (not shallow clone) due to GitVersioning
|
||||
- Some NuGet package security warnings are expected (SixLabors.ImageSharp vulnerabilities)
|
||||
- Frontend tests require Playwright browser installation: `npx playwright install`
|
||||
- Older Node.js versions may show engine compatibility warnings (check .nvmrc for current requirement)
|
||||
|
||||
## Timing Expectations
|
||||
|
||||
**NEVER CANCEL** these operations - they are expected to take time:
|
||||
|
||||
| Operation | Expected Time | Timeout Setting |
|
||||
| ----------------------- | ------------- | --------------- |
|
||||
| `dotnet restore` | 50 seconds | 90+ seconds |
|
||||
| `dotnet build` | 4.5 minutes | 10+ minutes |
|
||||
| `npm ci` | 11 seconds | 30+ seconds |
|
||||
| `npm run build:for:cms` | 1.25 minutes | 5+ minutes |
|
||||
| `npm test` | 2 minutes | 5+ minutes |
|
||||
| `npm run lint` | 1 minute | 5+ minutes |
|
||||
| Unit tests | 1 minute | 5+ minutes |
|
||||
| Integration tests | Variable | 10+ minutes |
|
||||
|
||||
Always wait for commands to complete rather than canceling and retrying.
|
||||
|
||||
@@ -4,7 +4,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
- v*/main
|
||||
paths:
|
||||
- src/Umbraco.Web.UI.Client/package.json
|
||||
- src/Umbraco.Web.UI.Client/package-lock.json
|
||||
@@ -14,7 +16,9 @@ on:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
- v*/main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- v*/dev
|
||||
- v*/main
|
||||
paths:
|
||||
- src/Umbraco.Web.UI.Client/package.json
|
||||
- src/Umbraco.Web.UI.Client/package-lock.json
|
||||
@@ -15,6 +16,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- v*/dev
|
||||
- v*/main
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
|
||||
@@ -12,7 +12,6 @@ permissions:
|
||||
|
||||
jobs:
|
||||
reconcile:
|
||||
if: github.repository == 'umbraco/Umbraco-CMS'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Reconcile release/* labels → discussions
|
||||
|
||||
@@ -51,10 +51,6 @@ tools/docfx/
|
||||
/build/csharp-docs/api/
|
||||
/build/csharp-docs/_site/
|
||||
|
||||
# Local config
|
||||
.claude/settings.local.json
|
||||
.env.local
|
||||
|
||||
# Build
|
||||
/build.out/
|
||||
/build.tmp/
|
||||
@@ -103,7 +99,6 @@ tools/docfx/
|
||||
playwright-report
|
||||
trace.zip
|
||||
/tests/Umbraco.Tests.AcceptanceTest/results
|
||||
/tests/Umbraco.Tests.AcceptanceTest/dist
|
||||
|
||||
# Ignore auto-generated schema
|
||||
/src/Umbraco.Cms.Targets/tasks/
|
||||
@@ -112,7 +107,6 @@ trace.zip
|
||||
/src/Umbraco.Web.UI/appsettings-schema.json
|
||||
/src/Umbraco.Web.UI/appsettings-schema.*.json
|
||||
/src/Umbraco.Web.UI/umbraco-package-schema.json
|
||||
/src/Umbraco.Web.UI.Client/umbraco-package-schema.json
|
||||
/tests/Umbraco.Tests.Integration/appsettings-schema.json
|
||||
/tests/Umbraco.Tests.Integration/appsettings-schema.*.json
|
||||
/tests/Umbraco.Tests.Integration/umbraco-package-schema.json
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"umbraco-cms": {
|
||||
"command": "npx",
|
||||
"args": ["@umbraco-cms/mcp-dev@17"]
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"@playwright/mcp@latest"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,108 +259,7 @@ Project ownership is distributed across teams. Check individual project director
|
||||
|
||||
---
|
||||
|
||||
## 5. Avoiding Breaking Changes
|
||||
|
||||
No binary breaking changes are allowed within a major version. Three patterns are used:
|
||||
|
||||
### 5.1 Obsolete Constructor + StaticServiceProvider
|
||||
|
||||
When a public class needs new dependencies, obsolete the existing constructor and add a new one. The old constructor delegates to the new one, resolving missing deps via `StaticServiceProvider`.
|
||||
|
||||
```csharp
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public MyService(IDependencyA depA)
|
||||
: this(
|
||||
depA,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDependencyB>())
|
||||
{
|
||||
}
|
||||
|
||||
public MyService(IDependencyA depA, IDependencyB depB)
|
||||
{
|
||||
_depA = depA;
|
||||
_depB = depB;
|
||||
}
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
- `ContentCollectionPresentationFactory` - added `FlagProviderCollection`
|
||||
- `CacheInstructionService` - added `ILastSyncedManager`, `IRepositoryCacheVersionService`
|
||||
- `DocumentPresentationFactory` - added `FlagProviderCollection`
|
||||
|
||||
**Rules**:
|
||||
- Old constructor marked `[Obsolete("... Scheduled for removal in Umbraco {current-major+2}.")]`
|
||||
- Old constructor calls new constructor via `: this(...)`
|
||||
- Uses `StaticServiceProvider.Instance.GetRequiredService<T>()` for new params only
|
||||
- DI registration must use the NEW constructor (old is for external consumers only)
|
||||
|
||||
### 5.2 Obsolete Method + New Overload
|
||||
|
||||
When a public method signature needs to change, add the new method/overload and obsolete the old. The obsolete method should call the new one with suitable defaults.
|
||||
|
||||
```csharp
|
||||
[Obsolete("Use the overload taking all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public void DoThing(string name)
|
||||
=> DoThing(name, extraParam: null);
|
||||
|
||||
public void DoThing(string name, string? extraParam)
|
||||
{
|
||||
// Real implementation here
|
||||
}
|
||||
```
|
||||
|
||||
**Rules**:
|
||||
- Old method marked `[Obsolete]` with removal schedule
|
||||
- DRY: old method calls new method, providing defaults for new parameters
|
||||
- All internal callers must be updated to use the new method
|
||||
- No callers should remain on the obsolete method within the codebase
|
||||
|
||||
### 5.3 Default Interface Implementation
|
||||
|
||||
When adding methods to a public interface, provide a default implementation so existing external implementations don't break.
|
||||
|
||||
```csharp
|
||||
public interface IMyService
|
||||
{
|
||||
// Existing method
|
||||
void ExistingMethod();
|
||||
|
||||
// New method with default implementation
|
||||
void NewMethod(string param)
|
||||
=> ExistingMethod(); // delegate to existing if possible
|
||||
}
|
||||
```
|
||||
|
||||
**Strategies for the default** (in order of preference):
|
||||
1. **Use existing interface methods** to satisfy the contract (even if not optimal)
|
||||
2. **Return a sensible default** like empty collection, null, etc.
|
||||
3. **Throw `NotImplementedException`** if no reasonable default exists
|
||||
|
||||
**Example**: `IContentService.SaveBlueprint` - new overload with `IContent? createdFromContent` has a default impl that calls the old method (ignoring the new param).
|
||||
|
||||
**Example**: `IDocumentPresentationFactory.CreateCulturePublishScheduleModels` - full default implementation with logic, uses `StaticServiceProvider` for dependency resolution within the interface.
|
||||
|
||||
**Rules**:
|
||||
- Add `// TODO (V{next-major}): Remove the default implementation when {obsolete method} is removed.` comment
|
||||
- Default impl should be functionally correct even if not optimal
|
||||
- If using `StaticServiceProvider` in a default impl, note this is temporary
|
||||
|
||||
### 5.4 General Rules
|
||||
|
||||
- **Removal policy**: Obsoleted members must remain for at least one full major version before removal. If obsoleted in version N, the earliest removal is version N+2. For example, something obsoleted in v17 is scheduled for removal in v19 (giving the whole of v18 as a deprecation period).
|
||||
- All `[Obsolete]` attributes must include **"Scheduled for removal in Umbraco {current+2}"**
|
||||
- Read `version.json` to determine the current major version
|
||||
- Suppress `CS0618` warnings where obsolete members must call each other:
|
||||
```csharp
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
=> OldMethod(param);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
```
|
||||
- Update ALL internal callers to use the new API - no internal code should use obsolete members
|
||||
|
||||
---
|
||||
|
||||
## 6. Project-Specific Notes
|
||||
## 5. Project-Specific Notes
|
||||
|
||||
### Centralized Package Management
|
||||
|
||||
@@ -393,18 +292,13 @@ The repository contains BOTH (actively supported):
|
||||
All APIs use **OpenIddict** (OAuth 2.0/OpenID Connect):
|
||||
- Reference tokens (not JWT) for better security
|
||||
- **Secure cookie-based token storage** (v17+) - tokens stored in HTTP-only cookies with `__Host-` prefix
|
||||
- Tokens are redacted from client-side responses and passed via secure cookies only (`[redacted]` placeholder)
|
||||
- Tokens are redacted from client-side responses and passed via secure cookies only
|
||||
- ASP.NET Core Data Protection for token encryption
|
||||
- Configured in `Umbraco.Cms.Api.Common`
|
||||
- API requests must include credentials (`credentials: include` for fetch)
|
||||
|
||||
**Load Balancing Requirement**: All servers must share the same Data Protection key ring.
|
||||
|
||||
**Frontend auth pitfalls** — see `src/Umbraco.Web.UI.Client/docs/edge-cases.md` (Auth & Cross-tab section) and `docs/security.md`. Key points:
|
||||
- Never call `validateToken()` per API request — it revokes the previous reference token (ID2019 errors)
|
||||
- `window.opener` is set for ANY `window.open()` target, not only OAuth popups — scope guards to the pathname too
|
||||
- BroadcastChannel does not deliver messages to the sender's own tab
|
||||
|
||||
### Content Caching Strategy
|
||||
|
||||
**HybridCache** (`Umbraco.PublishedCache.HybridCache`):
|
||||
@@ -419,65 +313,6 @@ APIs use `Asp.Versioning.Mvc`:
|
||||
- Delivery API: `/umbraco/delivery/api/v{version}/*`
|
||||
- OpenAPI/Swagger docs per version
|
||||
|
||||
### Backoffice npm Package Structure
|
||||
|
||||
The backoffice (`Umbraco.Web.UI.Client`) is published to npm as **`@umbraco-cms/backoffice`** with a plugin architecture:
|
||||
|
||||
#### Architecture Overview
|
||||
|
||||
- **Multi-workspace structure**: Subprojects in `src/libs/*`, `src/packages/*`, `src/external/*`
|
||||
- **Export model**: All exports defined in root `package.json` → `./exports` field
|
||||
- **Importmap-driven runtime**: Dependencies provided at runtime via importmap (single source of truth)
|
||||
- **Build-time types**: TypeScript types come from npm peerDependencies
|
||||
- **Plugin model**: Developers create plugins that import from `@umbraco-cms/backoffice/*` exports
|
||||
|
||||
#### Dependency Hoisting Strategy
|
||||
|
||||
When building for npm (`npm pack`), the `cleanse-pkg.js` script hoists subproject dependencies to root `peerDependencies` with intelligent version range conversion:
|
||||
|
||||
**Version Range Logic** (uses `semver` package):
|
||||
|
||||
1. **Pre-release (0.x.y)**: Convert to explicit range
|
||||
- Input: `^0.85.0` or `0.85.0`
|
||||
- Output: `>=0.85.0 <1.0.0`
|
||||
- Rationale: Pre-release caret only allows patch updates, explicit range allows minor upgrades within 0.x.x
|
||||
- Example: Plugin can use `@hey-api/openapi-ts@0.91.1` while backoffice uses `0.85.0`
|
||||
|
||||
2. **Stable with caret (^X.Y.Z where X ≥ 1)**: Keep as-is
|
||||
- Input: `^3.3.1`
|
||||
- Output: `^3.3.1` (unchanged)
|
||||
- Rationale: Caret already implements correct semantics for stable versions
|
||||
|
||||
3. **Stable exact versions (X.Y.Z where X ≥ 1)**: Add caret
|
||||
- Input: `3.16.0`
|
||||
- Output: `^3.16.0`
|
||||
- Rationale: Normalizes to conventional semver format
|
||||
|
||||
#### Key Dependencies
|
||||
|
||||
**Runtime via importmap** (types available from peerDependencies):
|
||||
- `lit`, `rxjs`, `@umbraco-ui/uui` - Core framework
|
||||
- `monaco-editor`, `@tiptap/*` - Feature-specific editors
|
||||
- `@hey-api/openapi-ts` - HTTP client type generation
|
||||
|
||||
**Build-time only** (not hoisted):
|
||||
- `vite`, `typescript`, `eslint` - Dev tooling
|
||||
|
||||
#### Plugin Development Implications
|
||||
|
||||
Plugin developers should:
|
||||
- **Declare explicit dependencies** in their own `package.json` (avoid relying on transitive deps)
|
||||
- **Understand the version ranges**: `>=0.85.0 <1.0.0` means they can use newer pre-release versions
|
||||
- **Know that types match npm ranges**, but runtime comes from importmap (managed by backoffice)
|
||||
- **When `@hey-api` hits 1.0.0**: Published constraint will automatically become `^1.0.0`
|
||||
|
||||
#### Implementation Details
|
||||
|
||||
- Script location: `src/Umbraco.Web.UI.Client/devops/publish/cleanse-pkg.js`
|
||||
- Runs as `prepack` hook before npm pack
|
||||
- Uses `semver.minVersion()` for robust version range parsing
|
||||
- Generates single source of truth for importmap versions
|
||||
|
||||
### Known Limitations
|
||||
|
||||
1. **Circular Dependencies**: Avoided via `Lazy<T>` or event notifications
|
||||
@@ -532,7 +367,6 @@ dotnet pack -c Release
|
||||
For detailed information about individual projects, see their CLAUDE.md files:
|
||||
- **Core Architecture**: `/src/Umbraco.Core/CLAUDE.md` - Service contracts, notification patterns
|
||||
- **API Infrastructure**: `/src/Umbraco.Cms.Api.Common/CLAUDE.md` - OpenAPI, authentication, serialization
|
||||
- **Backoffice Frontend**: `/src/Umbraco.Web.UI.Client/CLAUDE.md` - Lit web components, extension system, auth client
|
||||
|
||||
### Getting Help
|
||||
|
||||
|
||||
+28
-29
@@ -13,27 +13,27 @@
|
||||
</ItemGroup>
|
||||
<!-- Microsoft packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.4.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.1.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
<!-- Umbraco packages -->
|
||||
@@ -42,27 +42,27 @@
|
||||
</ItemGroup>
|
||||
<!-- Third-party packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.1" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.1" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
|
||||
<PackageVersion Include="Examine" Version="3.7.1" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.7.1" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
|
||||
<PackageVersion Include="JsonPatch.Net" Version="3.3.0" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
|
||||
<PackageVersion Include="MailKit" Version="4.15.1" />
|
||||
<PackageVersion Include="Markdig" Version="0.45.0" />
|
||||
<PackageVersion Include="MailKit" Version="4.14.1" />
|
||||
<PackageVersion Include="Markdig" Version="0.44.0" />
|
||||
<PackageVersion Include="Markdown" Version="2.2.1" />
|
||||
<PackageVersion Include="MessagePack" Version="3.1.4" />
|
||||
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
|
||||
<PackageVersion Include="MiniProfiler.Shared" Version="4.5.4" />
|
||||
<PackageVersion Include="ncrontab" Version="3.4.0" />
|
||||
<PackageVersion Include="NPoco" Version="6.2.0" />
|
||||
<PackageVersion Include="NPoco.SqlServer" Version="6.2.0" />
|
||||
<PackageVersion Include="NPoco" Version="6.1.0" />
|
||||
<PackageVersion Include="NPoco.SqlServer" Version="6.1.0" />
|
||||
<PackageVersion Include="OpenIddict.Abstractions" Version="7.2.0" />
|
||||
<PackageVersion Include="OpenIddict.AspNetCore" Version="7.2.0" />
|
||||
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="7.2.0" />
|
||||
<PackageVersion Include="Serilog" Version="4.3.1" />
|
||||
<PackageVersion Include="Serilog" Version="4.3.0" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Process" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
@@ -76,8 +76,7 @@
|
||||
<PackageVersion Include="Serilog.Sinks.Map" Version="2.0.0" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.2.0" />
|
||||
<!-- When updating this version, also update templates/UmbracoExtension/Umbraco.Extension.csproj -->
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.4" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
# MCP (Model Context Protocol) Setup
|
||||
|
||||
This repository includes configuration for [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers, enabling AI tooling integration for Umbraco CMS development workflows.
|
||||
|
||||
## Overview
|
||||
|
||||
MCP allows AI assistants (like Claude) to interact with external tools and services. This repository configures two MCP servers:
|
||||
|
||||
| Server | Purpose | Package |
|
||||
|--------|---------|---------|
|
||||
| **umbraco-cms** | Manage Umbraco content types, documents, and media | `@umbraco-cms/mcp-dev@17` |
|
||||
| **playwright** | Browser automation for testing and debugging | `@playwright/mcp@latest` |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Start Umbraco Locally
|
||||
|
||||
Ensure your local Umbraco instance is running at `https://localhost:44339` (or update the URL in your `.env.local`).
|
||||
|
||||
### 2. Configure Environment Variables
|
||||
|
||||
Copy the example environment file and customize it:
|
||||
|
||||
```bash
|
||||
cp .env.example .env.local
|
||||
```
|
||||
|
||||
Edit `.env.local` with your local settings:
|
||||
|
||||
```env
|
||||
UMBRACO_CLIENT_ID=umbraco-back-office-mcp
|
||||
UMBRACO_CLIENT_SECRET=<your-client-secret>
|
||||
UMBRACO_BASE_URL=https://localhost:44339
|
||||
NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
UMBRACO_INCLUDE_TOOL_COLLECTIONS=data-type,document-type,document,media-type,media
|
||||
```
|
||||
|
||||
### 3. Configure the OAuth Client in Umbraco
|
||||
|
||||
Create an OAuth client in your Umbraco instance with:
|
||||
- **Client ID**: `umbraco-back-office-mcp`
|
||||
- **Client Secret**: The value you set in `.env.local`
|
||||
- **Grant Type**: Client Credentials
|
||||
|
||||
## Environment Variables Reference
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `UMBRACO_CLIENT_ID` | OAuth client ID configured in Umbraco | `umbraco-back-office-mcp` |
|
||||
| `UMBRACO_CLIENT_SECRET` | OAuth client secret (keep secure!) | `your-secure-secret` |
|
||||
| `UMBRACO_BASE_URL` | URL of your local Umbraco instance | `https://localhost:44339` |
|
||||
| `NODE_TLS_REJECT_UNAUTHORIZED` | Set to `0` for self-signed certificates (local dev only) | `0` |
|
||||
| `UMBRACO_INCLUDE_TOOL_COLLECTIONS` | Comma-separated list of tool collections to enable | `data-type,document-type,document` |
|
||||
|
||||
### Tool Collections
|
||||
|
||||
The `UMBRACO_INCLUDE_TOOL_COLLECTIONS` variable controls which Umbraco MCP tools are available:
|
||||
|
||||
- `data-type` - Manage data types (property editors)
|
||||
- `document-type` - Manage document types (content types)
|
||||
- `document` - Manage content/documents
|
||||
- `media-type` - Manage media types
|
||||
- `media` - Manage media items
|
||||
|
||||
## Security Considerations
|
||||
|
||||
> **Warning**: This configuration is for **local development only**.
|
||||
|
||||
### Self-Signed Certificates
|
||||
|
||||
`NODE_TLS_REJECT_UNAUTHORIZED=0` disables SSL certificate validation. This is necessary for self-signed certificates in local development but:
|
||||
|
||||
- **Never use in production**
|
||||
- Affects all HTTPS connections made by Node.js processes
|
||||
- Consider trusting your local development certificate instead
|
||||
|
||||
### Client Secrets
|
||||
|
||||
- Never commit real secrets to source control
|
||||
- The `.env.local` file is gitignored for this reason
|
||||
- Use strong, unique secrets even in development
|
||||
- The example value `1234567890` in `.env.example` is a placeholder only
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
Umbraco-CMS/
|
||||
├── .mcp.json # MCP server configuration
|
||||
├── .env.example # Example environment variables (committed)
|
||||
├── .env.local # Your local environment variables (gitignored)
|
||||
├── .claude/
|
||||
│ ├── settings.json # Shared Claude AI permissions (committed)
|
||||
│ └── settings.local.json # Local Claude overrides (gitignored)
|
||||
├── .gitignore # Ignores .env.local and settings.local.json
|
||||
└── MCP.md # This documentation (you are here)
|
||||
```
|
||||
|
||||
## Claude AI Permissions
|
||||
|
||||
The `.claude/settings.json` file configures which MCP tools Claude can use automatically without prompting. This is shared across the team for consistent developer experience.
|
||||
|
||||
### Customizing Permissions Locally
|
||||
|
||||
Create `.claude/settings.local.json` to override permissions for your environment:
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"mcp__umbraco__get-all-document-types"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Connection refused" errors
|
||||
|
||||
- Ensure Umbraco is running at the configured `UMBRACO_BASE_URL`
|
||||
- Check that the port matches your local setup
|
||||
|
||||
### "Unauthorized" errors
|
||||
|
||||
- Verify the OAuth client is configured in Umbraco
|
||||
- Check that `UMBRACO_CLIENT_ID` and `UMBRACO_CLIENT_SECRET` match
|
||||
- Ensure the client has appropriate permissions
|
||||
|
||||
### "Certificate" errors
|
||||
|
||||
- For local development, set `NODE_TLS_REJECT_UNAUTHORIZED=0` in `.env.local`
|
||||
- Alternatively, trust your local development certificate
|
||||
|
||||
### MCP server not starting
|
||||
|
||||
- Ensure Node.js is installed (v22+ recommended, matching .nvmrc)
|
||||
- Run `npx @umbraco-cms/mcp-dev@17 --help` to verify the package works
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Model Context Protocol Documentation](https://modelcontextprotocol.io/)
|
||||
- [Umbraco MCP Package](https://www.npmjs.com/package/@umbraco-cms/mcp-dev)
|
||||
- [Playwright MCP](https://www.npmjs.com/package/@playwright/mcp)
|
||||
- [Claude Code Documentation](https://docs.anthropic.com/claude-code)
|
||||
+6
-115
@@ -107,17 +107,9 @@ stages:
|
||||
command: build
|
||||
projects: $(solution)
|
||||
arguments: "--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg"
|
||||
# Publish compiled DLLs for C# API documentation generation
|
||||
# Separate artifact to avoid increasing build_output size for all builds
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish DocFX DLLs
|
||||
condition: and(succeeded(), or(eq(variables['build.NBGV_PublicRelease'], 'True'), eq('${{ parameters.buildApiDocs }}', 'True')))
|
||||
inputs:
|
||||
targetPath: $(Build.SourcesDirectory)/src/Umbraco.Cms/bin/Release
|
||||
artifactName: csharp-docs-dlls
|
||||
- powershell: |
|
||||
dotnet tool install --global CycloneDX
|
||||
dotnet-CycloneDX $(solution) --spec-version 1.5 --output $(Build.ArtifactStagingDirectory)/bom --filename bom-dotnet.xml
|
||||
dotnet-CycloneDX $(solution) --output $(Build.ArtifactStagingDirectory)/bom --filename bom-dotnet.xml
|
||||
displayName: 'Generate Backend BOM'
|
||||
- powershell: |
|
||||
npm install --global @cyclonedx/cyclonedx-npm
|
||||
@@ -175,41 +167,6 @@ stages:
|
||||
artifact: bom-frontend
|
||||
displayName: 'Publish Frontend BOM'
|
||||
|
||||
- job: C
|
||||
displayName: Build Test Helpers Package
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false
|
||||
fetchDepth: 500
|
||||
- template: templates/e2e-install.yml
|
||||
parameters:
|
||||
nodeVersion: ${{ variables.nodeVersion }}
|
||||
npm_config_cache: ${{ variables.npm_config_cache }}
|
||||
- bash: |
|
||||
echo "##[command]Install nbgv"
|
||||
dotnet tool install --tool-path . nbgv
|
||||
echo "##[command]Running nbgv get-version"
|
||||
PACKAGE_VERSION=$(nbgv get-version -v NpmPackageVersion)
|
||||
echo "##[command]Running npm version"
|
||||
echo "##[debug]Version: $PACKAGE_VERSION"
|
||||
cd tests/Umbraco.Tests.AcceptanceTest
|
||||
npm version $PACKAGE_VERSION --allow-same-version --no-git-tag-version
|
||||
displayName: Set NPM Version
|
||||
- bash: |
|
||||
echo "##[command]Running npm pack"
|
||||
mkdir $(Build.ArtifactStagingDirectory)/npm-testhelpers
|
||||
npm pack --pack-destination $(Build.ArtifactStagingDirectory)/npm-testhelpers
|
||||
displayName: Run npm pack
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish Test Helpers npm artifact
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)/npm-testhelpers
|
||||
artifactName: npm-testhelpers
|
||||
|
||||
- stage: E2E_BOM
|
||||
displayName: E2E Tests BOM Generation
|
||||
dependsOn: []
|
||||
@@ -243,22 +200,12 @@ stages:
|
||||
variables:
|
||||
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'] ]
|
||||
jobs:
|
||||
# C# API Reference - uses pre-compiled DLLs for faster generation (csproj approach caused timeouts)
|
||||
# C# API Reference
|
||||
- job:
|
||||
displayName: Build C# API Reference
|
||||
pool:
|
||||
vmImage: "windows-latest"
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false
|
||||
fetchDepth: 1
|
||||
fetchFilter: tree:0
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download DocFX DLLs
|
||||
inputs:
|
||||
artifact: csharp-docs-dlls
|
||||
path: $(Build.SourcesDirectory)/src/Umbraco.Cms/bin/Release
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
inputs:
|
||||
@@ -268,7 +215,7 @@ stages:
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
dotnet tool install -g docfx --version 2.78.4
|
||||
dotnet tool install -g docfx
|
||||
if ($lastexitcode -ne 0){
|
||||
throw ("Error installing DocFX")
|
||||
}
|
||||
@@ -867,44 +814,12 @@ stages:
|
||||
npm publish "${files[0]}"
|
||||
displayName: Push to npm (MyGet)
|
||||
workingDirectory: $(Pipeline.Workspace)/npm
|
||||
- job: PublishTestHelpersNpm
|
||||
displayName: Push TestHelpers to pre-release feed (npm)
|
||||
steps:
|
||||
- checkout: none
|
||||
- download: current
|
||||
artifact: npm-testhelpers
|
||||
- bash: |
|
||||
# Check if we are on a nightly build
|
||||
if [ $isNightly = "False" ]; then
|
||||
echo "##[debug]Prerelease build detected"
|
||||
registry="https://www.myget.org/F/umbracoprereleases/npm/"
|
||||
else
|
||||
echo "##[debug]Nightly build detected"
|
||||
registry="https://www.myget.org/F/umbraconightly/npm/"
|
||||
fi
|
||||
echo "@umbraco-cms:registry=$registry" >> .npmrc
|
||||
env:
|
||||
isNightly: ${{parameters.isNightly}}
|
||||
workingDirectory: $(Pipeline.Workspace)/npm-testhelpers
|
||||
displayName: Add scoped registry to .npmrc
|
||||
- task: npmAuthenticate@0
|
||||
displayName: Authenticate with npm (MyGet)
|
||||
inputs:
|
||||
workingFile: "$(Pipeline.Workspace)/npm-testhelpers/.npmrc"
|
||||
customEndpoint: "MyGet (npm) - Umbracoprereleases, MyGet (npm) - Umbraconightly"
|
||||
- bash: |
|
||||
# Setup temp npm project to load in defaults from the local .npmrc
|
||||
npm init -y
|
||||
|
||||
# Find the first .tgz file in the current directory and publish it
|
||||
files=( ./*.tgz )
|
||||
npm publish "${files[0]}"
|
||||
displayName: Push test helpers to npm (MyGet)
|
||||
workingDirectory: $(Pipeline.Workspace)/npm-testhelpers
|
||||
|
||||
- stage: Deploy_NuGet
|
||||
displayName: NuGet release
|
||||
dependsOn: Deploy_MyGet
|
||||
dependsOn:
|
||||
- Deploy_MyGet
|
||||
- Build_Docs
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.nuGetDeploy}}))
|
||||
jobs:
|
||||
- job:
|
||||
@@ -955,29 +870,6 @@ stages:
|
||||
npm publish "${files[0]}"
|
||||
displayName: Push to npm
|
||||
workingDirectory: $(Pipeline.Workspace)/npm
|
||||
- job: PublishTestHelpers
|
||||
displayName: Push Test Helpers to NPM
|
||||
steps:
|
||||
- checkout: none
|
||||
- download: current
|
||||
artifact: npm-testhelpers
|
||||
- bash: echo "@umbraco-cms:registry=https://registry.npmjs.org" >> .npmrc
|
||||
workingDirectory: $(Pipeline.Workspace)/npm-testhelpers
|
||||
displayName: Add scoped registry to .npmrc
|
||||
- task: npmAuthenticate@0
|
||||
displayName: Authenticate with npm
|
||||
inputs:
|
||||
workingFile: $(Pipeline.Workspace)/npm-testhelpers/.npmrc
|
||||
customEndpoint: "NPM - Umbraco Backoffice"
|
||||
- script: |
|
||||
# Setup temp npm project to load in defaults from the local .npmrc
|
||||
npm init -y
|
||||
|
||||
# Find the first .tgz file in the current directory and publish it
|
||||
files=( ./*.tgz )
|
||||
npm publish "${files[0]}"
|
||||
displayName: Push test helpers to npm
|
||||
workingDirectory: $(Pipeline.Workspace)/npm-testhelpers
|
||||
|
||||
- stage: Upload_API_Docs
|
||||
pool:
|
||||
@@ -987,7 +879,6 @@ stages:
|
||||
displayName: Upload API Documentation
|
||||
dependsOn:
|
||||
- Build
|
||||
- Build_Docs
|
||||
- Deploy_NuGet
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.uploadApiDocs}}))
|
||||
jobs:
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
{
|
||||
"src": [
|
||||
{
|
||||
"src": "../../src/Umbraco.Cms/bin/Release",
|
||||
"src": "../../src",
|
||||
"files": [
|
||||
"**/Umbraco.*.dll"
|
||||
"**/*.csproj"
|
||||
],
|
||||
"exclude": [
|
||||
"**/Umbraco.Cms.StaticAssets.dll",
|
||||
"**/Umbraco.Cms.Targets.dll"
|
||||
"**/obj/**",
|
||||
"**/bin/**",
|
||||
"**/Umbraco.Web.csproj",
|
||||
"**/Umbraco.Web.UI.csproj",
|
||||
"**/Umbraco.Cms.StaticAssets.csproj",
|
||||
"**/JsonSchema.csproj"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<meta name="generator" content="docfx {{_docfxVersion}}">
|
||||
{{#_description}}<meta name="description" content="{{_description}}">{{/_description}}
|
||||
<link rel="icon" type="image/png" href="https://our.umbraco.com/assets/images/app-icons/favicon.png">
|
||||
<link rel="stylesheet" href="{{_rel}}styles/docfx.vendor.min.css">
|
||||
<link rel="stylesheet" href="{{_rel}}styles/docfx.vendor.css">
|
||||
<link rel="stylesheet" href="{{_rel}}styles/docfx.css">
|
||||
<link rel="stylesheet" href="{{_rel}}styles/main.css">
|
||||
<meta property="docfx:navrel" content="{{_navRel}}">
|
||||
|
||||
@@ -54,17 +54,11 @@ steps:
|
||||
- pwsh: |
|
||||
$sourcePath = "$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/tests/${{ parameters.testFolder }}/AdditionalSetup"
|
||||
$destinationPath = "UmbracoProject"
|
||||
$csharpFiles = Get-ChildItem -Path $sourcePath -Filter "*.cs" -Recurse
|
||||
$csharpFiles = Get-ChildItem -Path $sourcePath -Filter "*.cs"
|
||||
if ($csharpFiles) {
|
||||
$csharpFiles | ForEach-Object {
|
||||
$relativePath = $_.FullName.Substring($sourcePath.Length + 1)
|
||||
$targetPath = Join-Path -Path $destinationPath -ChildPath $relativePath
|
||||
$targetDir = Split-Path -Path $targetPath -Parent
|
||||
if (-not (Test-Path -Path $targetDir)) {
|
||||
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
||||
}
|
||||
Write-Host "Copying: $($_.FullName) -> $targetPath"
|
||||
Copy-Item -Path $_.FullName -Destination $targetPath -Force
|
||||
Write-Host "Copying: $($_.FullName)"
|
||||
Copy-Item -Path $_.FullName -Destination $destinationPath -Force
|
||||
}
|
||||
} else {
|
||||
Write-Host "No C# files found."
|
||||
|
||||
@@ -44,7 +44,7 @@ steps:
|
||||
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
|
||||
dotnet new nugetconfig
|
||||
dotnet nuget add source ./nupkg --name Local
|
||||
dotnet new install Umbraco.Templates@$cmsVersion
|
||||
dotnet new umbraco --name UmbracoProject --exclude-gitignore --no-restore --no-update-check
|
||||
dotnet new install Umbraco.Templates::$cmsVersion
|
||||
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
|
||||
displayName: Install Template
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
@@ -29,7 +29,7 @@ steps:
|
||||
"UMBRACO_USER_LOGIN=${{ parameters.PlaywrightUserEmail }}
|
||||
UMBRACO_USER_PASSWORD=${{ parameters.PlaywrightPassword }}
|
||||
URL=${{ parameters.ASPNETCORE_URLS }}
|
||||
STORAGE_STATE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
|
||||
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
|
||||
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
@@ -47,7 +47,3 @@ steps:
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
displayName: Restore NPM packages
|
||||
|
||||
- script: npm run build
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
displayName: Build test helpers
|
||||
|
||||
+1
-8
@@ -1,17 +1,10 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Accessors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to the <see cref="IOutputExpansionStrategy"/> for the current HTTP request context.
|
||||
/// </summary>
|
||||
public sealed class RequestContextOutputExpansionStrategyAccessor : RequestContextServiceAccessorBase<IOutputExpansionStrategy>, IOutputExpansionStrategyAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RequestContextOutputExpansionStrategyAccessor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
|
||||
public RequestContextOutputExpansionStrategyAccessor(IHttpContextAccessor httpContextAccessor)
|
||||
: base(httpContextAccessor)
|
||||
{
|
||||
|
||||
@@ -1,30 +1,17 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Accessors;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for accessing request-scoped services from the current HTTP context.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of service to access.</typeparam>
|
||||
public abstract class RequestContextServiceAccessorBase<T>
|
||||
where T : class
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RequestContextServiceAccessorBase{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
|
||||
protected RequestContextServiceAccessorBase(IHttpContextAccessor httpContextAccessor)
|
||||
=> _httpContextAccessor = httpContextAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the service from the current HTTP context's request services.
|
||||
/// </summary>
|
||||
/// <param name="requestStartNodeService">When this method returns, contains the service instance if found; otherwise, <c>null</c>.</param>
|
||||
/// <returns><c>true</c> if the service was found; otherwise, <c>false</c>.</returns>
|
||||
public bool TryGetValue([NotNullWhen(true)] out T? requestStartNodeService)
|
||||
{
|
||||
requestStartNodeService = _httpContextAccessor.HttpContext?.RequestServices.GetService<T>();
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
namespace Umbraco.Cms.Api.Common.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute used to map a class to a specific API for OpenAPI documentation generation.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||
public class MapToApiAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MapToApiAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="apiName">The name of the API to map to.</param>
|
||||
public MapToApiAttribute(string apiName) => ApiName = apiName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the API this class is mapped to.
|
||||
/// </summary>
|
||||
public string ApiName { get; }
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Builders;
|
||||
|
||||
/// <summary>
|
||||
/// A fluent builder for creating RFC 7807 <see cref="ProblemDetails"/> responses.
|
||||
/// </summary>
|
||||
public class ProblemDetailsBuilder
|
||||
{
|
||||
private string? _title;
|
||||
@@ -15,45 +12,24 @@ public class ProblemDetailsBuilder
|
||||
private string? _operationStatus;
|
||||
private IDictionary<string, object>? _extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the title of the problem details.
|
||||
/// </summary>
|
||||
/// <param name="title">A short, human-readable summary of the problem type.</param>
|
||||
/// <returns>The current builder instance for method chaining.</returns>
|
||||
public ProblemDetailsBuilder WithTitle(string title)
|
||||
{
|
||||
_title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the detail of the problem details.
|
||||
/// </summary>
|
||||
/// <param name="detail">A human-readable explanation specific to this occurrence of the problem.</param>
|
||||
/// <returns>The current builder instance for method chaining.</returns>
|
||||
public ProblemDetailsBuilder WithDetail(string detail)
|
||||
{
|
||||
_detail = detail;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the type of the problem details.
|
||||
/// </summary>
|
||||
/// <param name="type">A URI reference that identifies the problem type.</param>
|
||||
/// <returns>The current builder instance for method chaining.</returns>
|
||||
public ProblemDetailsBuilder WithType(string type)
|
||||
{
|
||||
_type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the operation status from an enum value.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The enum type representing operation statuses.</typeparam>
|
||||
/// <param name="operationStatus">The operation status enum value.</param>
|
||||
/// <returns>The current builder instance for method chaining.</returns>
|
||||
public ProblemDetailsBuilder WithOperationStatus<TEnum>(TEnum operationStatus)
|
||||
where TEnum : Enum
|
||||
{
|
||||
@@ -61,20 +37,9 @@ public class ProblemDetailsBuilder
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds request model validation errors to the problem details.
|
||||
/// </summary>
|
||||
/// <param name="errors">A dictionary of field names to error messages.</param>
|
||||
/// <returns>The current builder instance for method chaining.</returns>
|
||||
public ProblemDetailsBuilder WithRequestModelErrors(IDictionary<string, string[]> errors)
|
||||
=> WithExtension(nameof(HttpValidationProblemDetails.Errors).ToFirstLowerInvariant(), errors);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom extension to the problem details.
|
||||
/// </summary>
|
||||
/// <param name="key">The extension key.</param>
|
||||
/// <param name="value">The extension value.</param>
|
||||
/// <returns>The current builder instance for method chaining.</returns>
|
||||
public ProblemDetailsBuilder WithExtension(string key, object value)
|
||||
{
|
||||
_extensions ??= new Dictionary<string, object>();
|
||||
@@ -82,10 +47,6 @@ public class ProblemDetailsBuilder
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the <see cref="ProblemDetails"/> instance with all configured values.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="ProblemDetails"/> instance.</returns>
|
||||
public ProblemDetails Build()
|
||||
{
|
||||
var problemDetails = new ProblemDetails
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configures <see cref="ApiBehaviorOptions"/> for Umbraco APIs.
|
||||
/// </summary>
|
||||
public class ConfigureApiBehaviorOptions : IConfigureOptions<ApiBehaviorOptions>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(ApiBehaviorOptions options) =>
|
||||
// disable ProblemDetails as default result type for every non-success response (i.e. 404)
|
||||
// - see https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressmapclienterrors
|
||||
|
||||
@@ -5,21 +5,12 @@ using Umbraco.Cms.Api.Common.Json;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configures <see cref="MvcOptions"/> with named JSON input and output formatters for Umbraco APIs.
|
||||
/// </summary>
|
||||
public class ConfigureMvcJsonOptions : IConfigureOptions<MvcOptions>
|
||||
{
|
||||
private readonly string _jsonOptionsName;
|
||||
private readonly IOptionsMonitor<JsonOptions> _jsonOptions;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigureMvcJsonOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="jsonOptionsName">The name of the JSON options configuration to use.</param>
|
||||
/// <param name="jsonOptions">The JSON options monitor.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public ConfigureMvcJsonOptions(
|
||||
string jsonOptionsName,
|
||||
IOptionsMonitor<JsonOptions> jsonOptions,
|
||||
@@ -30,7 +21,6 @@ public class ConfigureMvcJsonOptions : IConfigureOptions<MvcOptions>
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Configure(MvcOptions options)
|
||||
{
|
||||
JsonOptions jsonOptions = _jsonOptions.Get(_jsonOptionsName);
|
||||
|
||||
@@ -4,24 +4,12 @@ using Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configures OpenIddict server options for Umbraco authentication.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Disables transport security requirement when HTTPS is not configured in global settings.
|
||||
/// Warning: This should only be used in development environments.
|
||||
/// </remarks>
|
||||
internal sealed class ConfigureOpenIddict : IConfigureOptions<OpenIddictServerAspNetCoreOptions>
|
||||
{
|
||||
private readonly IOptions<GlobalSettings> _globalSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigureOpenIddict"/> class.
|
||||
/// </summary>
|
||||
/// <param name="globalSettings">The global settings options.</param>
|
||||
public ConfigureOpenIddict(IOptions<GlobalSettings> globalSettings) => _globalSettings = globalSettings;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Configure(OpenIddictServerAspNetCoreOptions options)
|
||||
=> options.DisableTransportSecurityRequirement = _globalSettings.Value.UseHttps is false;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,6 @@ using Umbraco.Cms.Core.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configures Swagger/OpenAPI generation options for Umbraco APIs.
|
||||
/// </summary>
|
||||
public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
private readonly IOperationIdSelector _operationIdSelector;
|
||||
@@ -18,13 +15,6 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
|
||||
private readonly ISubTypesSelector _subTypesSelector;
|
||||
private readonly IDocumentInclusionSelector _documentInclusionSelector;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigureUmbracoSwaggerGenOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="operationIdSelector">The operation ID selector.</param>
|
||||
/// <param name="schemaIdSelector">The schema ID selector.</param>
|
||||
/// <param name="subTypesSelector">The sub-types selector for polymorphism support.</param>
|
||||
/// <param name="documentInclusionSelector">The document inclusion selector.</param>
|
||||
public ConfigureUmbracoSwaggerGenOptions(
|
||||
IOperationIdSelector operationIdSelector,
|
||||
ISchemaIdSelector schemaIdSelector,
|
||||
@@ -37,12 +27,6 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
|
||||
_documentInclusionSelector = documentInclusionSelector;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigureUmbracoSwaggerGenOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="operationIdSelector">The operation ID selector.</param>
|
||||
/// <param name="schemaIdSelector">The schema ID selector.</param>
|
||||
/// <param name="subTypesSelector">The sub-types selector for polymorphism support.</param>
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public ConfigureUmbracoSwaggerGenOptions(
|
||||
IOperationIdSelector operationIdSelector,
|
||||
@@ -56,7 +40,6 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Configure(SwaggerGenOptions swaggerGenOptions)
|
||||
{
|
||||
swaggerGenOptions.SwaggerDoc(
|
||||
@@ -81,14 +64,7 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
|
||||
swaggerGenOptions.SupportNonNullableReferenceTypes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a sort key for API actions.
|
||||
/// </summary>
|
||||
/// <param name="apiDesc">The API description.</param>
|
||||
/// <returns>A string used to sort API operations in the documentation.</returns>
|
||||
/// <remarks>
|
||||
/// See https://github.com/domaindrivendev/Swashbuckle.AspNetCore#change-operation-sort-order-eg-for-ui-sorting.
|
||||
/// </remarks>
|
||||
// see https://github.com/domaindrivendev/Swashbuckle.AspNetCore#change-operation-sort-order-eg-for-ui-sorting
|
||||
private static string ActionOrderBy(ApiDescription apiDesc)
|
||||
=> $"{apiDesc.GroupName}_{apiDesc.ActionDescriptor.AttributeRouteInfo?.Template ?? apiDesc.ActionDescriptor.RouteValues["controller"]}_{(apiDesc.ActionDescriptor.RouteValues.TryGetValue("action", out var action) ? action : null)}_{apiDesc.HttpMethod}";
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Contains default configuration values for the API.
|
||||
/// </summary>
|
||||
internal static class DefaultApiConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// The default API name used for endpoints not assigned to a specific API.
|
||||
/// </summary>
|
||||
public const string ApiName = "default";
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Server;
|
||||
using OpenIddict.Validation;
|
||||
using Umbraco.Cms.Core;
|
||||
@@ -14,19 +13,10 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Handles secure storage of back-office authentication tokens in HTTP-only cookies.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This handler intercepts OpenIddict token responses for the back-office client and stores
|
||||
/// access tokens, refresh tokens, and PKCE codes in encrypted HTTP-only cookies. The tokens
|
||||
/// are redacted from the response to prevent client-side JavaScript access.
|
||||
/// </remarks>
|
||||
internal sealed class HideBackOfficeTokensHandler
|
||||
: IOpenIddictServerHandler<OpenIddictServerEvents.ApplyTokenResponseContext>,
|
||||
IOpenIddictServerHandler<OpenIddictServerEvents.ApplyAuthorizationResponseContext>,
|
||||
IOpenIddictServerHandler<OpenIddictServerEvents.ExtractTokenRequestContext>,
|
||||
IOpenIddictServerHandler<OpenIddictServerEvents.ExtractRevocationRequestContext>,
|
||||
IOpenIddictValidationHandler<OpenIddictValidationEvents.ProcessAuthenticationContext>,
|
||||
INotificationHandler<UserLogoutSuccessNotification>
|
||||
{
|
||||
@@ -35,40 +25,25 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
// The __Host- prefix enforces secure cookies at browser level (requires Secure, Path=/, no Domain).
|
||||
// For local development over HTTP, we use a simpler prefix to avoid browser rejection.
|
||||
private const string SecureCookiePrefix = "__Host-";
|
||||
private readonly string _accessTokenCookieName = "umbAccessToken";
|
||||
private readonly string _refreshTokenCookieName = "umbRefreshToken";
|
||||
private readonly string _pkceCodeCookieName = "umbPkceCode";
|
||||
private const string AccessTokenCookieName = "umbAccessToken";
|
||||
private const string RefreshTokenCookieName = "umbRefreshToken";
|
||||
private const string PkceCodeCookieName = "umbPkceCode";
|
||||
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IDataProtectionProvider _dataProtectionProvider;
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
private readonly BackOfficeTokenCookieSettings _backOfficeTokenCookieSettings;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HideBackOfficeTokensHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
|
||||
/// <param name="dataProtectionProvider">The data protection provider for encrypting cookie values.</param>
|
||||
/// <param name="backOfficeTokenCookieSettings">The back-office token cookie settings.</param>
|
||||
/// <param name="globalSettings">The global settings.</param>
|
||||
public HideBackOfficeTokensHandler(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
IOptions<BackOfficeTokenCookieSettings> backOfficeTokenCookieSettings,
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
IOptions<GlobalSettings> globalSettings)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_dataProtectionProvider = dataProtectionProvider;
|
||||
_backOfficeTokenCookieSettings = backOfficeTokenCookieSettings.Value;
|
||||
_globalSettings = globalSettings.Value;
|
||||
|
||||
_accessTokenCookieName += _backOfficeTokenCookieSettings.SiteName;
|
||||
_refreshTokenCookieName += _backOfficeTokenCookieSettings.SiteName;
|
||||
_pkceCodeCookieName += _backOfficeTokenCookieSettings.SiteName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -88,13 +63,13 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
|
||||
if (context.Response.AccessToken is not null)
|
||||
{
|
||||
SetCookie(httpContext, _accessTokenCookieName, context.Response.AccessToken);
|
||||
SetCookie(httpContext, AccessTokenCookieName, context.Response.AccessToken);
|
||||
context.Response.AccessToken = RedactedTokenValue;
|
||||
}
|
||||
|
||||
if (context.Response.RefreshToken is not null)
|
||||
{
|
||||
SetCookie(httpContext, _refreshTokenCookieName, context.Response.RefreshToken);
|
||||
SetCookie(httpContext, RefreshTokenCookieName, context.Response.RefreshToken);
|
||||
context.Response.RefreshToken = RedactedTokenValue;
|
||||
}
|
||||
|
||||
@@ -116,7 +91,7 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
|
||||
if (context.Response.Code is not null)
|
||||
{
|
||||
SetCookie(GetHttpContext(), _pkceCodeCookieName, context.Response.Code);
|
||||
SetCookie(GetHttpContext(), PkceCodeCookieName, context.Response.Code);
|
||||
context.Response.Code = RedactedTokenValue;
|
||||
}
|
||||
|
||||
@@ -138,12 +113,12 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
|
||||
// Handle when the PKCE code is being exchanged for an access token.
|
||||
if (context.Request.Code == RedactedTokenValue
|
||||
&& TryGetCookie(httpContext, _pkceCodeCookieName, out var code))
|
||||
&& TryGetCookie(httpContext, PkceCodeCookieName, out var code))
|
||||
{
|
||||
context.Request.Code = code;
|
||||
|
||||
// We won't need the PKCE cookie after this, let's remove it.
|
||||
RemoveCookie(httpContext, _pkceCodeCookieName);
|
||||
RemoveCookie(httpContext, PkceCodeCookieName);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -154,7 +129,7 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
|
||||
// Handle when a refresh token is being exchanged for a new access token.
|
||||
if (context.Request.RefreshToken == RedactedTokenValue
|
||||
&& TryGetCookie(httpContext, _refreshTokenCookieName, out var refreshToken))
|
||||
&& TryGetCookie(httpContext, RefreshTokenCookieName, out var refreshToken))
|
||||
{
|
||||
context.Request.RefreshToken = refreshToken;
|
||||
}
|
||||
@@ -169,40 +144,6 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked when a token revocation request is received.
|
||||
/// </summary>
|
||||
public ValueTask HandleAsync(OpenIddictServerEvents.ExtractRevocationRequestContext context)
|
||||
{
|
||||
if (context.Request?.ClientId != Constants.OAuthClientIds.BackOffice)
|
||||
{
|
||||
// Only ever handle the back-office client.
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
HttpContext httpContext = GetHttpContext();
|
||||
|
||||
// Determine which cookie to read based on the token type hint.
|
||||
var cookieName = context.Request.TokenTypeHint == OpenIddictConstants.TokenTypeHints.RefreshToken
|
||||
? _refreshTokenCookieName
|
||||
: _accessTokenCookieName;
|
||||
|
||||
if (context.Request.Token == RedactedTokenValue
|
||||
&& TryGetCookie(httpContext, cookieName, out var token))
|
||||
{
|
||||
context.Request.Token = token;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we got here, either the token was not redacted, or nothing was found in the expected cookie.
|
||||
// If OpenIddict found a token, it could be an old token that is potentially still valid. For security
|
||||
// reasons, we cannot accept that; at this point, we expect the tokens to be explicitly redacted.
|
||||
context.Request.Token = null;
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked when extracting the auth context for a client request.
|
||||
/// </summary>
|
||||
@@ -214,7 +155,7 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
if (TryGetCookie(GetHttpContext(), _accessTokenCookieName, out var accessToken))
|
||||
if (TryGetCookie(GetHttpContext(), AccessTokenCookieName, out var accessToken))
|
||||
{
|
||||
context.AccessToken = accessToken;
|
||||
}
|
||||
@@ -222,7 +163,6 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Handle(UserLogoutSuccessNotification notification)
|
||||
{
|
||||
HttpContext? httpContext = _httpContextAccessor.HttpContext;
|
||||
@@ -234,8 +174,8 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
return;
|
||||
}
|
||||
|
||||
RemoveCookie(httpContext, _accessTokenCookieName);
|
||||
RemoveCookie(httpContext, _refreshTokenCookieName);
|
||||
RemoveCookie(httpContext, AccessTokenCookieName);
|
||||
RemoveCookie(httpContext, RefreshTokenCookieName);
|
||||
}
|
||||
|
||||
private HttpContext GetHttpContext()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -6,18 +6,8 @@ using Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IMvcBuilder"/>.
|
||||
/// </summary>
|
||||
public static class MvcBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds named JSON serialization options to the MVC builder.
|
||||
/// </summary>
|
||||
/// <param name="builder">The MVC builder.</param>
|
||||
/// <param name="settingsName">The name for the JSON options configuration.</param>
|
||||
/// <param name="configure">The action to configure the JSON options.</param>
|
||||
/// <returns>The MVC builder for method chaining.</returns>
|
||||
public static IMvcBuilder AddJsonOptions(this IMvcBuilder builder, string settingsName, Action<JsonOptions> configure)
|
||||
{
|
||||
builder.Services.Configure(settingsName, configure);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using OpenIddict.Server;
|
||||
using OpenIddict.Validation;
|
||||
using Umbraco.Cms.Core;
|
||||
@@ -6,23 +6,12 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Handles OpenIddict request processing to skip handling for non-authentication requests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This handler prevents OpenIddict from processing every request to the server,
|
||||
/// limiting its scope to back-office and well-known OpenID Connect endpoints.
|
||||
/// </remarks>
|
||||
public class ProcessRequestContextHandler
|
||||
: IOpenIddictServerHandler<OpenIddictServerEvents.ProcessRequestContext>, IOpenIddictValidationHandler<OpenIddictValidationEvents.ProcessRequestContext>
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly string[] _pathsToHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProcessRequestContextHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
|
||||
public ProcessRequestContextHandler(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
@@ -32,11 +21,6 @@ public class ProcessRequestContextHandler
|
||||
_pathsToHandle = [backOfficePathSegment, "/.well-known/openid-configuration", "/.well-known/jwks"];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the server process request context event.
|
||||
/// </summary>
|
||||
/// <param name="context">The process request context.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
|
||||
public ValueTask HandleAsync(OpenIddictServerEvents.ProcessRequestContext context)
|
||||
{
|
||||
if (SkipOpenIddictHandlingForRequest())
|
||||
@@ -47,11 +31,6 @@ public class ProcessRequestContextHandler
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the validation process request context event.
|
||||
/// </summary>
|
||||
/// <param name="context">The process request context.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
|
||||
public ValueTask HandleAsync(OpenIddictValidationEvents.ProcessRequestContext context)
|
||||
{
|
||||
if (SkipOpenIddictHandlingForRequest())
|
||||
|
||||
@@ -7,16 +7,8 @@ using Umbraco.Cms.Web.Common.ApplicationBuilder;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IUmbracoBuilder"/> to configure API services.
|
||||
/// </summary>
|
||||
public static class UmbracoBuilderApiExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Umbraco API OpenAPI/Swagger UI services to the builder.
|
||||
/// </summary>
|
||||
/// <param name="builder">The Umbraco builder.</param>
|
||||
/// <returns>The Umbraco builder for method chaining.</returns>
|
||||
public static IUmbracoBuilder AddUmbracoApiOpenApiUI(this IUmbracoBuilder builder)
|
||||
{
|
||||
if (builder.Services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(OperationIdSelector)))
|
||||
|
||||
@@ -9,26 +9,14 @@ using Umbraco.Cms.Api.Common.Security;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Infrastructure.BackgroundJobs;
|
||||
using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.DistributedJobs;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IUmbracoBuilder"/> to configure authentication services.
|
||||
/// </summary>
|
||||
public static class UmbracoBuilderAuthExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds OpenIddict authentication services for Umbraco APIs.
|
||||
/// </summary>
|
||||
/// <param name="builder">The Umbraco builder.</param>
|
||||
/// <returns>The Umbraco builder for method chaining.</returns>
|
||||
/// <remarks>
|
||||
/// Configures OpenIddict with authorization code flow (with PKCE), client credentials flow,
|
||||
/// reference tokens, and ASP.NET Core Data Protection for token encryption.
|
||||
/// </remarks>
|
||||
public static IUmbracoBuilder AddUmbracoOpenIddict(this IUmbracoBuilder builder)
|
||||
{
|
||||
if (builder.Services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(OpenIddictCleanupJob)) is false)
|
||||
@@ -145,12 +133,6 @@ public static class UmbracoBuilderAuthExtensions
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ExtractPostRequest<OpenIddictServerEvents.ExtractTokenRequestContext>.Descriptor.Order + 1);
|
||||
});
|
||||
options.AddEventHandler<OpenIddictServerEvents.ExtractRevocationRequestContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ExtractPostRequest<OpenIddictServerEvents.ExtractRevocationRequestContext>.Descriptor.Order + 1);
|
||||
});
|
||||
})
|
||||
|
||||
// Register the OpenIddict validation components.
|
||||
|
||||
@@ -33,4 +33,4 @@ public static class ActionDescriptorApiCommonExtensions
|
||||
|
||||
return mapToApiAttributes.SingleOrDefault()?.ApiName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,9 @@ using Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="MethodInfo"/> to work with API-related attributes.
|
||||
/// </summary>
|
||||
public static class MethodInfoApiCommonExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the API version values from <see cref="MapToApiVersionAttribute"/> applied to the method.
|
||||
/// </summary>
|
||||
/// <param name="methodInfo">The method info to inspect.</param>
|
||||
/// <returns>A pipe-separated string of API version values.</returns>
|
||||
|
||||
public static string GetMapToApiVersionAttributeValue(this MethodInfo methodInfo)
|
||||
{
|
||||
MapToApiVersionAttribute[] mapToApis = methodInfo.GetCustomAttributes(typeof(MapToApiVersionAttribute), inherit: true).Cast<MapToApiVersionAttribute>().ToArray();
|
||||
@@ -22,11 +15,6 @@ public static class MethodInfoApiCommonExtensions
|
||||
return string.Join("|", mapToApis.SelectMany(x => x.Versions));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the API name from <see cref="MapToApiAttribute"/> applied to the method's declaring type.
|
||||
/// </summary>
|
||||
/// <param name="methodInfo">The method info to inspect.</param>
|
||||
/// <returns>The API name if the attribute is present; otherwise, <c>null</c>.</returns>
|
||||
public static string? GetMapToApiAttributeValue(this MethodInfo methodInfo)
|
||||
{
|
||||
MapToApiAttribute[] mapToApis = (methodInfo.DeclaringType?.GetCustomAttributes(typeof(MapToApiAttribute), inherit: true) ?? Array.Empty<object>()).Cast<MapToApiAttribute>().ToArray();
|
||||
@@ -34,15 +22,6 @@ public static class MethodInfoApiCommonExtensions
|
||||
return mapToApis.SingleOrDefault()?.ApiName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the method's declaring type has a <see cref="MapToApiAttribute"/> with the specified API name.
|
||||
/// </summary>
|
||||
/// <param name="methodInfo">The method info to inspect.</param>
|
||||
/// <param name="apiName">The API name to check for.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the attribute is present and matches the specified API name,
|
||||
/// or if the attribute is not present and the API name matches the default API name; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool HasMapToApiAttribute(this MethodInfo methodInfo, string apiName)
|
||||
{
|
||||
var value = methodInfo.GetMapToApiAttributeValue();
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
namespace Umbraco.Cms.Api.Common.Filters;
|
||||
namespace Umbraco.Cms.Api.Common.Filters;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute used to specify the named JSON serialization options for a controller.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class JsonOptionsNameAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JsonOptionsNameAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="jsonOptionsName">The name of the JSON options configuration to use.</param>
|
||||
public JsonOptionsNameAttribute(string jsonOptionsName) => JsonOptionsName = jsonOptionsName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the JSON options configuration.
|
||||
/// </summary>
|
||||
public string JsonOptionsName { get; }
|
||||
}
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Api.Common.Filters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="HttpContext"/> related to JSON serialization.
|
||||
/// </summary>
|
||||
public static class HttpContextJsonExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the named JSON options configuration for the current endpoint.
|
||||
/// </summary>
|
||||
/// <param name="context">The HTTP context.</param>
|
||||
/// <returns>The JSON options name if specified via <see cref="JsonOptionsNameAttribute"/>; otherwise, <c>null</c>.</returns>
|
||||
public static string? CurrentJsonOptionsName(this HttpContext context)
|
||||
=> context.GetEndpoint()?.Metadata.GetMetadata<JsonOptionsNameAttribute>()?.JsonOptionsName;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,20 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Json;
|
||||
|
||||
/// <summary>
|
||||
/// A JSON input formatter that only processes requests for endpoints with matching named JSON options.
|
||||
/// </summary>
|
||||
internal sealed class NamedSystemTextJsonInputFormatter : SystemTextJsonInputFormatter
|
||||
{
|
||||
private readonly string _jsonOptionsName;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NamedSystemTextJsonInputFormatter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="jsonOptionsName">The name of the JSON options configuration this formatter handles.</param>
|
||||
/// <param name="options">The JSON options.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public NamedSystemTextJsonInputFormatter(string jsonOptionsName, JsonOptions options, ILogger<NamedSystemTextJsonInputFormatter> logger)
|
||||
: base(options, logger) =>
|
||||
_jsonOptionsName = jsonOptionsName;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanRead(InputFormatterContext context)
|
||||
=> context.HttpContext.CurrentJsonOptionsName() == _jsonOptionsName && base.CanRead(context);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Json;
|
||||
|
||||
/// <summary>
|
||||
/// A JSON output formatter that only processes responses for endpoints with matching named JSON options.
|
||||
/// </summary>
|
||||
internal sealed class NamedSystemTextJsonOutputFormatter : SystemTextJsonOutputFormatter
|
||||
{
|
||||
private readonly string _jsonOptionsName;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NamedSystemTextJsonOutputFormatter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="jsonOptionsName">The name of the JSON options configuration this formatter handles.</param>
|
||||
/// <param name="jsonSerializerOptions">The JSON serializer options.</param>
|
||||
public NamedSystemTextJsonOutputFormatter(string jsonOptionsName, JsonSerializerOptions jsonSerializerOptions) : base(jsonSerializerOptions)
|
||||
{
|
||||
_jsonOptionsName = jsonOptionsName;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
|
||||
=> context.HttpContext.CurrentJsonOptionsName() == _jsonOptionsName && base.CanWriteResult(context);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -16,13 +16,6 @@ public sealed class EmptyCreatedAtActionResult : ActionResult
|
||||
private readonly object _routeValues;
|
||||
private readonly string _resourceIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EmptyCreatedAtActionResult"/> class.
|
||||
/// </summary>
|
||||
/// <param name="actionName">The name of the action to generate the URL for.</param>
|
||||
/// <param name="controllerName">The name of the controller to generate the URL for.</param>
|
||||
/// <param name="routeValues">The route values to use for URL generation.</param>
|
||||
/// <param name="resourceIdentifier">The identifier of the created resource.</param>
|
||||
public EmptyCreatedAtActionResult(string actionName, string controllerName, object routeValues, string resourceIdentifier)
|
||||
{
|
||||
_actionName = actionName;
|
||||
@@ -31,7 +24,6 @@ public sealed class EmptyCreatedAtActionResult : ActionResult
|
||||
_resourceIdentifier = resourceIdentifier;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ExecuteResult(ActionContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.OpenApi;
|
||||
@@ -6,16 +6,8 @@ using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// A schema filter that converts enum schemas to string type with enum member names.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This filter ensures enums are represented as strings in the OpenAPI schema,
|
||||
/// using <see cref="EnumMemberAttribute"/> values when available.
|
||||
/// </remarks>
|
||||
public class EnumSchemaFilter : ISchemaFilter
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Apply(IOpenApiSchema model, SchemaFilterContext context)
|
||||
{
|
||||
if (model is not OpenApiSchema schema || context.Type.IsEnum is false)
|
||||
|
||||
@@ -2,22 +2,9 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a handler for generating OpenAPI operation IDs.
|
||||
/// </summary>
|
||||
public interface IOperationIdHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether this handler can generate an operation ID for the specified API description.
|
||||
/// </summary>
|
||||
/// <param name="apiDescription">The API description to check.</param>
|
||||
/// <returns><c>true</c> if this handler can handle the API description; otherwise, <c>false</c>.</returns>
|
||||
bool CanHandle(ApiDescription apiDescription);
|
||||
bool CanHandle(ApiDescription apiDescription);
|
||||
|
||||
/// <summary>
|
||||
/// Generates an operation ID for the specified API description.
|
||||
/// </summary>
|
||||
/// <param name="apiDescription">The API description to generate an operation ID for.</param>
|
||||
/// <returns>The generated operation ID.</returns>
|
||||
string Handle(ApiDescription apiDescription);
|
||||
string Handle(ApiDescription apiDescription);
|
||||
}
|
||||
|
||||
@@ -3,15 +3,7 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a selector for choosing operation IDs from registered handlers.
|
||||
/// </summary>
|
||||
public interface IOperationIdSelector
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects an operation ID for the specified API description.
|
||||
/// </summary>
|
||||
/// <param name="apiDescription">The API description to generate an operation ID for.</param>
|
||||
/// <returns>The operation ID, or <c>null</c> if none could be determined.</returns>
|
||||
string? OperationId(ApiDescription apiDescription);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a handler for generating OpenAPI schema IDs.
|
||||
/// </summary>
|
||||
public interface ISchemaIdHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether this handler can generate a schema ID for the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check.</param>
|
||||
/// <returns><c>true</c> if this handler can handle the type; otherwise, <c>false</c>.</returns>
|
||||
bool CanHandle(Type type);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a schema ID for the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to generate a schema ID for.</param>
|
||||
/// <returns>The generated schema ID.</returns>
|
||||
string Handle(Type type);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a selector for choosing schema IDs from registered handlers.
|
||||
/// </summary>
|
||||
public interface ISchemaIdSelector
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects a schema ID for the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to generate a schema ID for.</param>
|
||||
/// <returns>The schema ID.</returns>
|
||||
string SchemaId(Type type);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a handler for discovering sub-types for polymorphic OpenAPI schemas.
|
||||
/// </summary>
|
||||
public interface ISubTypesHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether this handler can discover sub-types for the specified type and document.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check.</param>
|
||||
/// <param name="documentName">The OpenAPI document name.</param>
|
||||
/// <returns><c>true</c> if this handler can handle the type; otherwise, <c>false</c>.</returns>
|
||||
bool CanHandle(Type type, string documentName);
|
||||
|
||||
/// <summary>
|
||||
/// Discovers sub-types for the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to discover sub-types for.</param>
|
||||
/// <returns>An enumerable of discovered sub-types.</returns>
|
||||
IEnumerable<Type> Handle(Type type);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a selector for choosing sub-types from registered handlers.
|
||||
/// </summary>
|
||||
public interface ISubTypesSelector
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects sub-types for the specified type for polymorphic OpenAPI schema generation.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to find sub-types for.</param>
|
||||
/// <returns>An enumerable of sub-types.</returns>
|
||||
IEnumerable<Type> SubTypes(Type type);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -11,13 +11,8 @@ public class MimeTypeDocumentFilter : IDocumentFilter
|
||||
{
|
||||
private readonly string _documentName;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MimeTypeDocumentFilter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="documentName">The name of the OpenAPI document to filter.</param>
|
||||
public MimeTypeDocumentFilter(string documentName) => _documentName = documentName;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
if (context.DocumentName != _documentName)
|
||||
@@ -29,7 +24,7 @@ public class MimeTypeDocumentFilter : IDocumentFilter
|
||||
.SelectMany(path => path.Value.Operations?.Values ?? Enumerable.Empty<OpenApiOperation>())
|
||||
.ToArray();
|
||||
|
||||
static void RemoveUnwantedMimeTypes(IDictionary<string, OpenApiMediaType>? content)
|
||||
void RemoveUnwantedMimeTypes(IDictionary<string, OpenApiMediaType>? content)
|
||||
{
|
||||
if (content is null || content.ContainsKey("application/json") is false)
|
||||
{
|
||||
|
||||
@@ -6,24 +6,14 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Default handler for generating OpenAPI operation IDs for Umbraco API controllers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Left unsealed on purpose, so it is extendable by consuming APIs.
|
||||
/// </remarks>
|
||||
// NOTE: Left unsealed on purpose, so it is extendable.
|
||||
public class OperationIdHandler : IOperationIdHandler
|
||||
{
|
||||
private readonly ApiVersioningOptions _apiVersioningOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OperationIdHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="apiVersioningOptions">The API versioning options.</param>
|
||||
public OperationIdHandler(IOptions<ApiVersioningOptions> apiVersioningOptions)
|
||||
=> _apiVersioningOptions = apiVersioningOptions.Value;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool CanHandle(ApiDescription apiDescription)
|
||||
{
|
||||
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
|
||||
@@ -34,16 +24,9 @@ public class OperationIdHandler : IOperationIdHandler
|
||||
return CanHandle(apiDescription, controllerActionDescriptor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this handler can process the API description based on the controller namespace.
|
||||
/// </summary>
|
||||
/// <param name="apiDescription">The API description.</param>
|
||||
/// <param name="controllerActionDescriptor">The controller action descriptor.</param>
|
||||
/// <returns><c>true</c> if the controller is in an Umbraco.Cms.Api namespace; otherwise, <c>false</c>.</returns>
|
||||
protected virtual bool CanHandle(ApiDescription apiDescription, ControllerActionDescriptor controllerActionDescriptor)
|
||||
=> controllerActionDescriptor.ControllerTypeInfo.Namespace?.StartsWith("Umbraco.Cms.Api") is true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual string Handle(ApiDescription apiDescription)
|
||||
=> UmbracoOperationId(apiDescription);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
|
||||
@@ -3,30 +3,19 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Selects an operation ID for an API description using registered handlers.
|
||||
/// </summary>
|
||||
public class OperationIdSelector : IOperationIdSelector
|
||||
{
|
||||
private readonly IEnumerable<IOperationIdHandler> _operationIdHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OperationIdSelector"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Use non-obsolete constructor. Scheduled for removal in Umbraco 18.")]
|
||||
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")]
|
||||
public OperationIdSelector()
|
||||
: this(Enumerable.Empty<IOperationIdHandler>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OperationIdSelector"/> class.
|
||||
/// </summary>
|
||||
/// <param name="operationIdHandlers">The registered operation ID handlers.</param>
|
||||
public OperationIdSelector(IEnumerable<IOperationIdHandler> operationIdHandlers)
|
||||
=> _operationIdHandlers = operationIdHandlers;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual string? OperationId(ApiDescription apiDescription)
|
||||
{
|
||||
IOperationIdHandler? handler = _operationIdHandlers.FirstOrDefault(h => h.CanHandle(apiDescription));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
@@ -10,14 +10,9 @@ public class RemoveSecuritySchemesDocumentFilter : IDocumentFilter
|
||||
{
|
||||
private readonly string _documentName;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RemoveSecuritySchemesDocumentFilter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="documentName">The name of the OpenAPI document to filter.</param>
|
||||
public RemoveSecuritySchemesDocumentFilter(string documentName)
|
||||
=> _documentName = documentName;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
if (context.DocumentName != _documentName)
|
||||
|
||||
@@ -3,20 +3,12 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Default handler for generating OpenAPI schema IDs for Umbraco types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Left unsealed on purpose, so it is extendable by consuming APIs.
|
||||
/// Adds "Model" suffix to avoid TypeScript name clashes and removes invalid characters.
|
||||
/// </remarks>
|
||||
// NOTE: Left unsealed on purpose, so it is extendable.
|
||||
public class SchemaIdHandler : ISchemaIdHandler
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public virtual bool CanHandle(Type type)
|
||||
=> type.Namespace?.StartsWith("Umbraco.Cms") is true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual string Handle(Type type)
|
||||
=> UmbracoSchemaId(type);
|
||||
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Selects a schema ID for a type using registered handlers.
|
||||
/// </summary>
|
||||
public class SchemaIdSelector : ISchemaIdSelector
|
||||
{
|
||||
private readonly IEnumerable<ISchemaIdHandler> _schemaIdHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SchemaIdSelector"/> class.
|
||||
/// </summary>
|
||||
/// <param name="schemaIdHandlers">The registered schema ID handlers.</param>
|
||||
public SchemaIdSelector(IEnumerable<ISchemaIdHandler> schemaIdHandlers)
|
||||
=> _schemaIdHandlers = schemaIdHandlers;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual string SchemaId(Type type)
|
||||
{
|
||||
ISchemaIdHandler? handler = _schemaIdHandlers.FirstOrDefault(h => h.CanHandle(type));
|
||||
|
||||
@@ -2,33 +2,19 @@ using Umbraco.Cms.Api.Common.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Default handler for discovering sub-types for polymorphic OpenAPI schemas.
|
||||
/// </summary>
|
||||
public class SubTypesHandler : ISubTypesHandler
|
||||
{
|
||||
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SubTypesHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="umbracoJsonTypeInfoResolver">The JSON type info resolver for finding sub-types.</param>
|
||||
public SubTypesHandler(IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
|
||||
=> _umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this handler can process the specified type based on namespace.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check.</param>
|
||||
/// <returns><c>true</c> if the type is in an Umbraco.Cms namespace; otherwise, <c>false</c>.</returns>
|
||||
protected virtual bool CanHandle(Type type)
|
||||
=> type.Namespace?.StartsWith("Umbraco.Cms") is true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual bool CanHandle(Type type, string documentName)
|
||||
=> CanHandle(type);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual IEnumerable<Type> Handle(Type type)
|
||||
=> _umbracoJsonTypeInfoResolver.FindSubTypes(type);
|
||||
}
|
||||
|
||||
@@ -8,9 +8,6 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Selects sub-types for polymorphic OpenAPI schemas using registered handlers.
|
||||
/// </summary>
|
||||
public class SubTypesSelector : ISubTypesSelector
|
||||
{
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
@@ -18,13 +15,6 @@ public class SubTypesSelector : ISubTypesSelector
|
||||
private readonly IEnumerable<ISubTypesHandler> _subTypeHandlers;
|
||||
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SubTypesSelector"/> class.
|
||||
/// </summary>
|
||||
/// <param name="hostingEnvironment">The hosting environment.</param>
|
||||
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
|
||||
/// <param name="subTypeHandlers">The registered sub-type handlers.</param>
|
||||
/// <param name="umbracoJsonTypeInfoResolver">The JSON type info resolver for finding sub-types.</param>
|
||||
public SubTypesSelector(
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
@@ -37,7 +27,6 @@ public class SubTypesSelector : ISubTypesSelector
|
||||
_umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<Type> SubTypes(Type type)
|
||||
{
|
||||
var backOfficePath = _hostingEnvironment.GetBackOfficePath();
|
||||
@@ -46,7 +35,8 @@ public class SubTypesSelector : ISubTypesSelector
|
||||
if (_httpContextAccessor.HttpContext?.Request.Path.StartsWithSegments(swaggerPath) ?? false)
|
||||
{
|
||||
// Split the path into segments
|
||||
var segments = _httpContextAccessor.HttpContext.Request.Path.Value![swaggerPath.Length..]
|
||||
var segments = _httpContextAccessor.HttpContext.Request.Path.Value!
|
||||
.Substring(swaggerPath.Length)
|
||||
.TrimStart(Constants.CharArrays.ForwardSlash)
|
||||
.Split(Constants.CharArrays.ForwardSlash);
|
||||
|
||||
|
||||
@@ -13,15 +13,8 @@ using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Pipeline filter that configures Swagger/OpenAPI endpoints for Umbraco APIs.
|
||||
/// </summary>
|
||||
public class SwaggerRouteTemplatePipelineFilter : UmbracoPipelineFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SwaggerRouteTemplatePipelineFilter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the pipeline filter.</param>
|
||||
public SwaggerRouteTemplatePipelineFilter(string name)
|
||||
: base(name)
|
||||
=> PostPipeline = PostPipelineAction;
|
||||
@@ -43,36 +36,15 @@ public class SwaggerRouteTemplatePipelineFilter : UmbracoPipelineFilter
|
||||
applicationBuilder.UseSwaggerUI(swaggerUiOptions => SwaggerUiConfiguration(swaggerUiOptions, swaggerGenOptions.Value, applicationBuilder));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether Swagger is enabled for the application.
|
||||
/// </summary>
|
||||
/// <param name="applicationBuilder">The application builder.</param>
|
||||
/// <returns><c>true</c> if Swagger is enabled; otherwise, <c>false</c>.</returns>
|
||||
protected virtual bool SwaggerIsEnabled(IApplicationBuilder applicationBuilder)
|
||||
=> applicationBuilder.ApplicationServices.GetRequiredService<IWebHostEnvironment>().IsProduction() is false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the route template for Swagger JSON endpoints.
|
||||
/// </summary>
|
||||
/// <param name="applicationBuilder">The application builder.</param>
|
||||
/// <returns>The Swagger route template.</returns>
|
||||
protected virtual string SwaggerRouteTemplate(IApplicationBuilder applicationBuilder)
|
||||
=> $"{GetBackOfficePath(applicationBuilder).TrimStart(Constants.CharArrays.ForwardSlash)}/swagger/{{documentName}}/swagger.json";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the route prefix for the Swagger UI.
|
||||
/// </summary>
|
||||
/// <param name="applicationBuilder">The application builder.</param>
|
||||
/// <returns>The Swagger UI route prefix.</returns>
|
||||
protected virtual string SwaggerUiRoutePrefix(IApplicationBuilder applicationBuilder)
|
||||
=> $"{GetBackOfficePath(applicationBuilder).TrimStart(Constants.CharArrays.ForwardSlash)}/swagger";
|
||||
|
||||
/// <summary>
|
||||
/// Configures the Swagger UI options.
|
||||
/// </summary>
|
||||
/// <param name="swaggerUiOptions">The Swagger UI options to configure.</param>
|
||||
/// <param name="swaggerGenOptions">The Swagger generation options.</param>
|
||||
/// <param name="applicationBuilder">The application builder.</param>
|
||||
protected virtual void SwaggerUiConfiguration(
|
||||
SwaggerUIOptions swaggerUiOptions,
|
||||
SwaggerGenOptions swaggerGenOptions,
|
||||
|
||||
@@ -4,64 +4,30 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Implements output expansion strategy for element-only rendering in the Delivery API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This strategy handles the expansion and filtering of properties when rendering content
|
||||
/// through the Delivery API based on expand and fields query parameters.
|
||||
/// </remarks>
|
||||
public class ElementOnlyOutputExpansionStrategy : IOutputExpansionStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// The parameter value indicating all properties should be included.
|
||||
/// </summary>
|
||||
protected const string All = "$all";
|
||||
|
||||
/// <summary>
|
||||
/// The parameter value indicating no properties should be included.
|
||||
/// </summary>
|
||||
protected const string None = "";
|
||||
|
||||
/// <summary>
|
||||
/// The name of the expand query parameter.
|
||||
/// </summary>
|
||||
protected const string ExpandParameterName = "expand";
|
||||
|
||||
/// <summary>
|
||||
/// The name of the fields query parameter.
|
||||
/// </summary>
|
||||
protected const string FieldsParameterName = "fields";
|
||||
|
||||
private readonly IApiPropertyRenderer _propertyRenderer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stack of expand property nodes for tracking nested expansions.
|
||||
/// </summary>
|
||||
protected Stack<Node?> ExpandProperties { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stack of include property nodes for tracking nested field selections.
|
||||
/// </summary>
|
||||
protected Stack<Node?> IncludeProperties { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ElementOnlyOutputExpansionStrategy"/> class.
|
||||
/// </summary>
|
||||
/// <param name="propertyRenderer">The property renderer for converting property values.</param>
|
||||
public ElementOnlyOutputExpansionStrategy(
|
||||
IApiPropertyRenderer propertyRenderer)
|
||||
{
|
||||
_propertyRenderer = propertyRenderer;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual IDictionary<string, object?> MapContentProperties(IPublishedContent content)
|
||||
=> content.ItemType == PublishedItemType.Content
|
||||
? MapProperties(content.Properties)
|
||||
: throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual IDictionary<string, object?> MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true)
|
||||
{
|
||||
if (media.ItemType != PublishedItemType.Media)
|
||||
@@ -79,7 +45,6 @@ public class ElementOnlyOutputExpansionStrategy : IOutputExpansionStrategy
|
||||
: new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual IDictionary<string, object?> MapElementProperties(IPublishedElement element)
|
||||
=> MapProperties(element.Properties, true);
|
||||
|
||||
@@ -122,27 +87,12 @@ public class ElementOnlyOutputExpansionStrategy : IOutputExpansionStrategy
|
||||
private object? GetPropertyValue(IPublishedProperty property)
|
||||
=> _propertyRenderer.GetPropertyValue(property, ExpandProperties.Peek() is not null);
|
||||
|
||||
/// <summary>
|
||||
/// Represents a node in the parsed expand/fields parameter tree structure.
|
||||
/// </summary>
|
||||
protected sealed class Node
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the key of this node.
|
||||
/// </summary>
|
||||
public string Key { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the child nodes of this node.
|
||||
/// </summary>
|
||||
public List<Node> Items { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Parses an expand/fields parameter value into a node tree structure.
|
||||
/// </summary>
|
||||
/// <param name="value">The parameter value to parse.</param>
|
||||
/// <returns>The root node of the parsed tree.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the value has invalid syntax.</exception>
|
||||
public static Node Parse(string value)
|
||||
{
|
||||
// verify that there are as many start brackets as there are end brackets
|
||||
|
||||
@@ -2,77 +2,35 @@ using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Security;
|
||||
|
||||
/// <summary>
|
||||
/// Contains OAuth/OpenID Connect endpoint paths for Umbraco APIs.
|
||||
/// </summary>
|
||||
public static class Paths
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains endpoint paths for back-office authentication.
|
||||
/// </summary>
|
||||
public static class BackOfficeApi
|
||||
{
|
||||
/// <summary>
|
||||
/// The base endpoint template for back-office security endpoints.
|
||||
/// </summary>
|
||||
public const string EndpointTemplate = "security/back-office";
|
||||
|
||||
/// <summary>
|
||||
/// The authorization endpoint path.
|
||||
/// </summary>
|
||||
public static readonly string AuthorizationEndpoint = EndpointPath($"{EndpointTemplate}/authorize");
|
||||
|
||||
/// <summary>
|
||||
/// The token endpoint path.
|
||||
/// </summary>
|
||||
public static readonly string TokenEndpoint = EndpointPath($"{EndpointTemplate}/token");
|
||||
|
||||
/// <summary>
|
||||
/// The logout/sign-out endpoint path.
|
||||
/// </summary>
|
||||
public static readonly string LogoutEndpoint = EndpointPath($"{EndpointTemplate}/signout");
|
||||
|
||||
/// <summary>
|
||||
/// The token revocation endpoint path.
|
||||
/// </summary>
|
||||
public static readonly string RevokeEndpoint = EndpointPath($"{EndpointTemplate}/revoke");
|
||||
|
||||
private static string EndpointPath(string relativePath) => $"/umbraco{Constants.Web.ManagementApiPath}v1/{relativePath}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains endpoint paths for member authentication.
|
||||
/// </summary>
|
||||
public static class MemberApi
|
||||
{
|
||||
/// <summary>
|
||||
/// The base endpoint template for member security endpoints.
|
||||
/// </summary>
|
||||
public const string EndpointTemplate = "security/member";
|
||||
|
||||
/// <summary>
|
||||
/// The authorization endpoint path.
|
||||
/// </summary>
|
||||
public static readonly string AuthorizationEndpoint = EndpointPath($"{EndpointTemplate}/authorize");
|
||||
|
||||
/// <summary>
|
||||
/// The token endpoint path.
|
||||
/// </summary>
|
||||
public static readonly string TokenEndpoint = EndpointPath($"{EndpointTemplate}/token");
|
||||
|
||||
/// <summary>
|
||||
/// The logout/sign-out endpoint path.
|
||||
/// </summary>
|
||||
public static readonly string LogoutEndpoint = EndpointPath($"{EndpointTemplate}/signout");
|
||||
|
||||
/// <summary>
|
||||
/// The token revocation endpoint path.
|
||||
/// </summary>
|
||||
public static readonly string RevokeEndpoint = EndpointPath($"{EndpointTemplate}/revoke");
|
||||
|
||||
/// <summary>
|
||||
/// The user info endpoint path.
|
||||
/// </summary>
|
||||
public static readonly string UserinfoEndpoint = EndpointPath($"{EndpointTemplate}/userinfo");
|
||||
|
||||
// NOTE: we're NOT using /api/v1.0/ here because it will clash with the Delivery API docs
|
||||
|
||||
@@ -2,22 +2,9 @@ using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Extends <see cref="IJsonTypeInfoResolver"/> with Umbraco-specific type resolution for polymorphic JSON serialization.
|
||||
/// </summary>
|
||||
public interface IUmbracoJsonTypeInfoResolver : IJsonTypeInfoResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Finds all sub-types of the specified type for polymorphic serialization.
|
||||
/// </summary>
|
||||
/// <param name="type">The base type to find sub-types for.</param>
|
||||
/// <returns>An enumerable of sub-types.</returns>
|
||||
IEnumerable<Type> FindSubTypes(Type type);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type discriminator value used for polymorphic serialization.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to get the discriminator value for.</param>
|
||||
/// <returns>The discriminator value, or <c>null</c> if not applicable.</returns>
|
||||
string? GetTypeDiscriminatorValue(Type type);
|
||||
}
|
||||
|
||||
@@ -8,26 +8,14 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Implements JSON type info resolution for Umbraco with support for polymorphic serialization.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This resolver discovers sub-types of interfaces for polymorphic JSON serialization,
|
||||
/// caching results for performance. It also handles type discriminator values for OpenAPI schema generation.
|
||||
/// </remarks>
|
||||
public sealed class UmbracoJsonTypeInfoResolver : DefaultJsonTypeInfoResolver, IUmbracoJsonTypeInfoResolver
|
||||
{
|
||||
private readonly ITypeFinder _typeFinder;
|
||||
private readonly ConcurrentDictionary<Type, ISet<Type>> _subTypesCache = new ConcurrentDictionary<Type, ISet<Type>>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoJsonTypeInfoResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="typeFinder">The type finder for discovering sub-types.</param>
|
||||
public UmbracoJsonTypeInfoResolver(ITypeFinder typeFinder)
|
||||
=> _typeFinder = typeFinder;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<Type> FindSubTypes(Type type)
|
||||
{
|
||||
JsonDerivedTypeAttribute[] explicitJsonDerivedTypes = type
|
||||
@@ -56,7 +44,6 @@ public sealed class UmbracoJsonTypeInfoResolver : DefaultJsonTypeInfoResolver, I
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? GetTypeDiscriminatorValue(Type type)
|
||||
{
|
||||
JsonDerivedTypeAttribute? jsonDerivedTypeAttribute = type
|
||||
@@ -75,7 +62,6 @@ public sealed class UmbracoJsonTypeInfoResolver : DefaultJsonTypeInfoResolver, I
|
||||
return typeof(IOpenApiDiscriminator).IsAssignableFrom(type) ? type.Name : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
|
||||
{
|
||||
JsonTypeInfo result = base.GetTypeInfo(type, options);
|
||||
|
||||
@@ -1,28 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a paged collection of items with total count.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of items in the collection.</typeparam>
|
||||
public class PagedViewModel<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of items available.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public long Total { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the items in the current page.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates an empty paged view model.
|
||||
/// </summary>
|
||||
/// <returns>An empty <see cref="PagedViewModel{T}"/> instance.</returns>
|
||||
public static PagedViewModel<T> Empty() => new();
|
||||
}
|
||||
|
||||
@@ -2,33 +2,16 @@ using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a subset of items with counts of items before and after the subset.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of items in the collection.</typeparam>
|
||||
public class SubsetViewModel<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of items before this subset.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public long TotalBefore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of items after this subset.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public long TotalAfter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the items in the subset.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates an empty subset view model.
|
||||
/// </summary>
|
||||
/// <returns>An empty <see cref="SubsetViewModel{T}"/> instance.</returns>
|
||||
public static SubsetViewModel<T> Empty() => new();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Features;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Cms.Web.Common.Controllers;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
@@ -15,7 +14,6 @@ namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
[JsonOptionsName(Constants.JsonOptionsNames.DeliveryApi)]
|
||||
[MapToApi(DeliveryApiConfiguration.ApiName)]
|
||||
[Authorize(Policy = AuthorizationPolicies.UmbracoFeatureEnabled)]
|
||||
[MaintenanceModeActionFilter]
|
||||
public abstract class DeliveryApiControllerBase : Controller, IUmbracoFeature
|
||||
{
|
||||
protected string DecodePath(string path)
|
||||
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Umbraco.Cms.Web.Common.ApplicationBuilder;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IApplicationBuilder" /> extensions for the Umbraco Delivery API.
|
||||
/// </summary>
|
||||
public static class DeliveryApiApplicationBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets up routes for the Umbraco Delivery API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method maps attribute-routed controllers including the Delivery API endpoints.
|
||||
/// Call this when using <c>AddDeliveryApi()</c> without <c>AddBackOffice()</c>, as the
|
||||
/// backoffice endpoints normally handle the controller mapping.
|
||||
/// </remarks>
|
||||
/// <param name="builder">The Umbraco endpoint builder context.</param>
|
||||
/// <returns>The <see cref="IUmbracoEndpointBuilderContext" /> for chaining.</returns>
|
||||
public static IUmbracoEndpointBuilderContext UseDeliveryApiEndpoints(this IUmbracoEndpointBuilderContext builder)
|
||||
{
|
||||
builder.EndpointRouteBuilder.MapControllers();
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Infrastructure.Security;
|
||||
using Umbraco.Cms.Web.Common.ApplicationBuilder;
|
||||
|
||||
@@ -29,20 +30,8 @@ namespace Umbraco.Extensions;
|
||||
|
||||
public static class UmbracoBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Add services for the Umbraco Delivery API (headless content delivery).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method assumes that either <c>AddBackOffice()</c> or <c>AddCore()</c> has already been called.
|
||||
/// It registers Delivery API-specific services such as controllers, output caching, and member authentication.
|
||||
/// </remarks>
|
||||
/// <param name="builder">The Umbraco builder.</param>
|
||||
/// <returns>The Umbraco builder.</returns>
|
||||
public static IUmbracoBuilder AddDeliveryApi(this IUmbracoBuilder builder)
|
||||
{
|
||||
// Delivery API supports member authentication for protected content
|
||||
builder.AddMembersIdentity();
|
||||
|
||||
builder.Services.AddScoped<IRequestStartItemProvider, RequestStartItemProvider>();
|
||||
builder.Services.AddScoped<RequestContextOutputExpansionStrategy>();
|
||||
builder.Services.AddScoped<RequestContextOutputExpansionStrategyV2>();
|
||||
|
||||
@@ -4,11 +4,7 @@ using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
/// <summary>
|
||||
/// An action filter attribute that verifies public or preview access to the Delivery API, returning
|
||||
/// a <c>401 Unauthorized</c> result if access is denied.
|
||||
/// </summary>
|
||||
public sealed class DeliveryApiAccessAttribute : TypeFilterAttribute
|
||||
internal sealed class DeliveryApiAccessAttribute : TypeFilterAttribute
|
||||
{
|
||||
public DeliveryApiAccessAttribute()
|
||||
: base(typeof(DeliveryApiAccessFilter))
|
||||
|
||||
@@ -4,11 +4,7 @@ using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
/// <summary>
|
||||
/// An action filter attribute that verifies public access to the media Delivery API, returning
|
||||
/// a <c>401 Unauthorized</c> result if access is denied.
|
||||
/// </summary>
|
||||
public sealed class DeliveryApiMediaAccessAttribute : TypeFilterAttribute
|
||||
internal sealed class DeliveryApiMediaAccessAttribute : TypeFilterAttribute
|
||||
{
|
||||
public DeliveryApiMediaAccessAttribute()
|
||||
: base(typeof(DeliveryApiMediaAccessFilter))
|
||||
|
||||
@@ -2,10 +2,7 @@ using Umbraco.Cms.Web.Common.Routing;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// A routing attribute that ensures consistent Delivery API endpoint paths.
|
||||
/// </summary>
|
||||
public sealed class VersionedDeliveryApiRouteAttribute : BackOfficeRouteAttribute
|
||||
internal sealed class VersionedDeliveryApiRouteAttribute : BackOfficeRouteAttribute
|
||||
{
|
||||
public VersionedDeliveryApiRouteAttribute(string template)
|
||||
: base($"delivery/api/v{{version:apiVersion}}/{template.TrimStart('/')}")
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
|
||||
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Umbraco.Cms.Api.Management.Controllers.Document.GetPublicAccessDocumentController.GetPublicAccess(System.Threading.CancellationToken,System.Guid)</Target>
|
||||
<Left>lib/net10.0/Umbraco.Cms.Api.Management.dll</Left>
|
||||
<Right>lib/net10.0/Umbraco.Cms.Api.Management.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Umbraco.Cms.Api.Management.Controllers.UrlSegment.ResizeImagingController.Urls(System.Collections.Generic.HashSet{System.Guid},System.Int32,System.Int32,System.Nullable{Umbraco.Cms.Core.Models.ImageCropMode})</Target>
|
||||
<Left>lib/net10.0/Umbraco.Cms.Api.Management.dll</Left>
|
||||
<Right>lib/net10.0/Umbraco.Cms.Api.Management.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
</Suppressions>
|
||||
@@ -140,20 +140,15 @@ public class ConfigureBackOfficeCookieOptions : IConfigureNamedOptions<CookieAut
|
||||
|
||||
await securityStampValidator.ValidateAsync(ctx);
|
||||
|
||||
// Only reset timestamps when a renewal was already triggered (by the SecurityStampValidator
|
||||
// or by EnsureTicketRenewalIfKeepUserLoggedIn above).
|
||||
// When the SecurityStampValidator refreshes the principal, it sets ShouldRenew but updates
|
||||
// IssuedUtc without updating ExpiresUtc, causing the effective cookie lifetime to shrink
|
||||
// with each validation. The manual reset here fixes that drift.
|
||||
// IMPORTANT: Do NOT unconditionally set ShouldRenew or reset IssuedUtc - doing so prevents
|
||||
// the SecurityStampValidator from ever exceeding its ValidationInterval during active use,
|
||||
// which breaks AllowConcurrentLogins enforcement.
|
||||
if (ctx.ShouldRenew)
|
||||
{
|
||||
DateTimeOffset now = _timeProvider.GetUtcNow();
|
||||
ctx.Properties.IssuedUtc = now;
|
||||
ctx.Properties.ExpiresUtc = now.Add(_globalSettings.TimeOut);
|
||||
}
|
||||
// We have to manually specify Issued and Expires,
|
||||
// because the SecurityStampValidator refreshes the principal every 30 minutes,
|
||||
// When the principal is refreshed the Issued is update to time of refresh, however, the Expires remains unchanged
|
||||
// When we then try and renew, the difference of issued and expires effectively becomes the new ExpireTimeSpan
|
||||
// meaning we effectively lose 30 minutes of our ExpireTimeSpan for EVERY principal refresh if we don't
|
||||
// https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authentication/Cookies/src/CookieAuthenticationHandler.cs#L115
|
||||
ctx.Properties.IssuedUtc = _timeProvider.GetUtcNow();
|
||||
ctx.Properties.ExpiresUtc = _timeProvider.GetUtcNow().Add(_globalSettings.TimeOut);
|
||||
ctx.ShouldRenew = true;
|
||||
},
|
||||
OnSigningIn = ctx =>
|
||||
{
|
||||
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Used to configure <see cref="CookieAuthenticationOptions" /> for the back office "exposed" authentication type
|
||||
/// </summary>
|
||||
public class ConfigureBackOfficeExposedCookieOptions : IConfigureNamedOptions<CookieAuthenticationOptions>
|
||||
{
|
||||
private readonly SecuritySettings _securitySettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigureBackOfficeExposedCookieOptions" /> class.
|
||||
/// </summary>
|
||||
/// <param name="securitySettings">The <see cref="SecuritySettings" /> options</param>
|
||||
public ConfigureBackOfficeExposedCookieOptions(IOptions<SecuritySettings> securitySettings)
|
||||
=> _securitySettings = securitySettings.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(string? name, CookieAuthenticationOptions options)
|
||||
{
|
||||
if (name != Constants.Security.BackOfficeExposedAuthenticationType)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Configure(options);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(CookieAuthenticationOptions options)
|
||||
{
|
||||
options.Cookie.Name = _securitySettings.AuthCookieName.IsNullOrWhiteSpace()
|
||||
? Constants.Security.BackOfficeExposedCookieName
|
||||
: $"{_securitySettings.AuthCookieName}{Constants.Security.BackOfficeExposedCookieNamePostfix}";
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
|
||||
options.SlidingExpiration = true;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -23,6 +23,6 @@ public class ConfigureBackOfficeSecurityStampValidatorOptions : IConfigureOption
|
||||
public void Configure(BackOfficeSecurityStampValidatorOptions options)
|
||||
{
|
||||
options.TimeProvider = _timeProvider;
|
||||
ConfigureSecurityStampOptions.ConfigureOptions(options, _securitySettings.GetUserAllowConcurrentLogins());
|
||||
ConfigureSecurityStampOptions.ConfigureOptions(options, _securitySettings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,23 @@ using Umbraco.Cms.Core.Hosting;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management;
|
||||
|
||||
[BindProperties]
|
||||
public class BackOfficeLoginModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the "ReturnUrl" query parameter or defaults to the configured Umbraco directory.
|
||||
/// </summary>
|
||||
[FromQuery(Name = "ReturnUrl")]
|
||||
public string? ReturnUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The configured Umbraco directory.
|
||||
/// </summary>
|
||||
public string? UmbracoUrl { get; set; }
|
||||
|
||||
public bool UserIsAlreadyLoggedIn { get; set; }
|
||||
}
|
||||
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
[Route(LoginPath)]
|
||||
public class BackOfficeLoginController : Controller
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management;
|
||||
|
||||
[BindProperties]
|
||||
public class BackOfficeLoginModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the "ReturnUrl" query parameter or defaults to the configured Umbraco directory.
|
||||
/// </summary>
|
||||
[FromQuery(Name = "ReturnUrl")]
|
||||
public string? ReturnUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The configured Umbraco directory.
|
||||
/// </summary>
|
||||
public string? UmbracoUrl { get; set; }
|
||||
|
||||
public bool UserIsAlreadyLoggedIn { get; set; }
|
||||
}
|
||||
@@ -79,11 +79,11 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.CannotDeleteWhenReferenced => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Cannot delete a referenced content item")
|
||||
.WithDetail("Cannot delete a referenced content item, while the setting ContentSettings.DisableDeleteWhenReferenced is enabled.")
|
||||
.WithDetail("Cannot delete a referenced document, while the setting ContentSettings.DisableDeleteWhenReferenced is enabled.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.CannotMoveToRecycleBinWhenReferenced => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Cannot move a referenced content item to the recycle bin")
|
||||
.WithDetail("Cannot move a referenced content item to the recycle bin, while the setting ContentSettings.DisableDeleteWhenReferenced is enabled.")
|
||||
.WithTitle("Cannot move a referenced document to the recycle bin")
|
||||
.WithDetail("Cannot move a referenced document to the recycle bin, while the setting ContentSettings.DisableUnpublishWhenReferenced is enabled.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.Unknown => StatusCode(
|
||||
StatusCodes.Status500InternalServerError,
|
||||
|
||||
@@ -21,11 +21,13 @@ public class AllCultureController : CultureControllerBase
|
||||
_cultureService = cultureService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all cultures available for creating languages.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<CultureReponseModel>), StatusCodes.Status200OK)]
|
||||
[EndpointSummary("Gets a paginated collection of cultures available for creating languages.")]
|
||||
[EndpointDescription("Gets a paginated collection containing the English and localized names of all available cultures.")]
|
||||
public Task<PagedViewModel<CultureReponseModel>> GetAll(CancellationToken cancellationToken, int skip = 0, int take = 100)
|
||||
{
|
||||
CultureInfo[] all = _cultureService.GetValidCultureInfos();
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an API controller for retrieving the full details for multiple data types by key.
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
public class BatchDataTypesController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BatchDataTypesController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dataTypeService">The data type service.</param>
|
||||
/// <param name="umbracoMapper">The presentation model mapper.</param>
|
||||
public BatchDataTypesController(IDataTypeService dataTypeService, IUmbracoMapper umbracoMapper)
|
||||
{
|
||||
_dataTypeService = dataTypeService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
}
|
||||
|
||||
[HttpGet("batch")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(BatchResponseModel<DataTypeResponseModel>), StatusCodes.Status200OK)]
|
||||
[EndpointSummary("Gets multiple data types.")]
|
||||
[EndpointDescription("Gets multiple data types identified by the provided Ids.")]
|
||||
public async Task<IActionResult> Batch(
|
||||
CancellationToken cancellationToken,
|
||||
[FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
{
|
||||
Guid[] requestedIds = [.. ids];
|
||||
|
||||
if (requestedIds.Length == 0)
|
||||
{
|
||||
return Ok(new BatchResponseModel<DataTypeResponseModel>());
|
||||
}
|
||||
|
||||
IEnumerable<IDataType> dataTypes = await _dataTypeService.GetAllAsync(requestedIds);
|
||||
|
||||
List<IDataType> ordered = OrderByRequestedIds(dataTypes, requestedIds);
|
||||
|
||||
var responseModels = ordered.Select(dt => _umbracoMapper.Map<DataTypeResponseModel>(dt)!).ToList();
|
||||
|
||||
return Ok(new BatchResponseModel<DataTypeResponseModel>
|
||||
{
|
||||
Total = responseModels.Count,
|
||||
Items = responseModels,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType;
|
||||
@@ -24,8 +24,6 @@ public class ByKeyDataTypeController : DataTypeControllerBase
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(DataTypeResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[EndpointSummary("Gets a data type.")]
|
||||
[EndpointDescription("Gets a data type identified by the provided Id.")]
|
||||
public async Task<IActionResult> ByKey(CancellationToken cancellationToken, Guid id)
|
||||
{
|
||||
IDataType? dataType = await _dataTypeService.GetAsync(id);
|
||||
|
||||
+1
-3
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -18,8 +18,6 @@ public class ConfigurationDataTypeController : DataTypeControllerBase
|
||||
[HttpGet("configuration")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(DatatypeConfigurationResponseModel), StatusCodes.Status200OK)]
|
||||
[EndpointSummary("Gets the data type configuration.")]
|
||||
[EndpointDescription("Gets the configuration settings for data types.")]
|
||||
public Task<IActionResult> Configuration(CancellationToken cancellationToken)
|
||||
{
|
||||
var responseModel = new DatatypeConfigurationResponseModel
|
||||
|
||||
@@ -29,8 +29,6 @@ public class CopyDataTypeController : DataTypeControllerBase
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[EndpointSummary("Copies a data type.")]
|
||||
[EndpointDescription("Creates a duplicate of an existing data type identified by the provided unique Id. The copied data type will be given a new Id and have ' (copy)' appended to its name. Optionally, the copy can be placed in a specific container by providing a target container Id.")]
|
||||
public async Task<IActionResult> Copy(CancellationToken cancellationToken, Guid id, CopyDataTypeRequestModel copyDataTypeRequestModel)
|
||||
{
|
||||
IDataType? source = await _dataTypeService.GetAsync(id);
|
||||
|
||||
@@ -33,8 +33,6 @@ public class CreateDataTypeController : DataTypeControllerBase
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[EndpointSummary("Creates a new data type.")]
|
||||
[EndpointDescription("Creates a new data type with the configuration specified in the request model.")]
|
||||
public async Task<IActionResult> Create(CancellationToken cancellationToken, CreateDataTypeRequestModel createDataTypeRequestModel)
|
||||
{
|
||||
var attempt = await _dataTypePresentationFactory.CreateAsync(createDataTypeRequestModel);
|
||||
|
||||
@@ -29,8 +29,6 @@ public class DeleteDataTypeController : DataTypeControllerBase
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[EndpointSummary("Deletes a data type.")]
|
||||
[EndpointDescription("Deletes a data type identified by the provided Id.")]
|
||||
public async Task<IActionResult> Delete(CancellationToken cancellationToken, Guid id)
|
||||
{
|
||||
Attempt<IDataType?, DataTypeOperationStatus> result = await _dataTypeService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
+1
-3
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
@@ -26,8 +26,6 @@ public class FilterDataTypeFilterController : DataTypeFilterControllerBase
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<DataTypeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
[EndpointSummary("Gets a filtered collection of data types.")]
|
||||
[EndpointDescription("Filters data types based on the provided criteria with support for pagination.")]
|
||||
public async Task<IActionResult> Filter(
|
||||
CancellationToken cancellationToken,
|
||||
int skip = 0,
|
||||
|
||||
+1
-3
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Folder;
|
||||
@@ -21,7 +21,5 @@ public class ByKeyDataTypeFolderController : DataTypeFolderControllerBase
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(FolderResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[EndpointSummary("Gets a data type folder.")]
|
||||
[EndpointDescription("Gets a data type folder identified by the provided Id.")]
|
||||
public async Task<IActionResult> ByKey(CancellationToken cancellationToken, Guid id) => await GetFolderAsync(id);
|
||||
}
|
||||
|
||||
+1
-3
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Folder;
|
||||
@@ -22,8 +22,6 @@ public class CreateDataTypeFolderController : DataTypeFolderControllerBase
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[EndpointSummary("Creates a data type folder.")]
|
||||
[EndpointDescription("Creates a new data type folder with the provided name and parent location.")]
|
||||
public async Task<IActionResult> Create(CancellationToken cancellationToken, CreateFolderRequestModel createFolderRequestModel)
|
||||
=> await CreateFolderAsync<ByKeyDataTypeFolderController>(
|
||||
createFolderRequestModel,
|
||||
|
||||
+1
-3
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
@@ -21,7 +21,5 @@ public class DeleteDataTypeFolderController : DataTypeFolderControllerBase
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[EndpointSummary("Deletes a data type folder.")]
|
||||
[EndpointDescription("Deletes a data type folder identified by the provided Id.")]
|
||||
public async Task<IActionResult> Delete(CancellationToken cancellationToken, Guid id) => await DeleteFolderAsync(id);
|
||||
}
|
||||
|
||||
+1
-3
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Folder;
|
||||
@@ -22,8 +22,6 @@ public class UpdateDataTypeFolderController : DataTypeFolderControllerBase
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[EndpointSummary("Updates a data type folder.")]
|
||||
[EndpointDescription("Updates a data type folder identified by the provided Id with the details provided in the request model.")]
|
||||
public async Task<IActionResult> Update(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
|
||||
@@ -21,8 +21,6 @@ public class IsUsedDataTypeController : DataTypeControllerBase
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[EndpointSummary("Checks if a data type is used.")]
|
||||
[EndpointDescription("Checks if the data type identified by the provided Id is used in any content, media, or member types.")]
|
||||
public async Task<IActionResult> IsUsed(CancellationToken cancellationToken, Guid id)
|
||||
{
|
||||
Attempt<bool, DataTypeOperationStatus> result = await _dataTypeUsageService.HasSavedValuesAsync(id);
|
||||
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Services.Entities;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Item;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Item;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class AncestorsDataTypeItemController : DatatypeItemControllerBase
|
||||
{
|
||||
private readonly IItemAncestorService _itemAncestorService;
|
||||
|
||||
public AncestorsDataTypeItemController(IItemAncestorService itemAncestorService)
|
||||
=> _itemAncestorService = itemAncestorService;
|
||||
|
||||
[HttpGet("ancestors")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<ItemAncestorsResponseModel<NamedItemResponseModel>>), StatusCodes.Status200OK)]
|
||||
[EndpointSummary("Gets ancestors for a collection of data type items.")]
|
||||
[EndpointDescription("Gets the ancestor chains for data type items identified by the provided Ids.")]
|
||||
public async Task<IActionResult> Ancestors(
|
||||
CancellationToken cancellationToken,
|
||||
[FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
{
|
||||
if (ids.Count is 0)
|
||||
{
|
||||
return Ok(Enumerable.Empty<ItemAncestorsResponseModel<NamedItemResponseModel>>());
|
||||
}
|
||||
|
||||
IEnumerable<ItemAncestorsResponseModel<NamedItemResponseModel>> result = await _itemAncestorService.GetAncestorsAsync(
|
||||
UmbracoObjectTypes.DataType,
|
||||
UmbracoObjectTypes.DataTypeContainer,
|
||||
ids);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType.Item;
|
||||
@@ -23,8 +23,6 @@ public class ItemDatatypeItemController : DatatypeItemControllerBase
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<DataTypeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
[EndpointSummary("Gets a collection of data type items.")]
|
||||
[EndpointDescription("Gets a collection of data type items identified by the provided Ids.")]
|
||||
public async Task<IActionResult> Item(
|
||||
CancellationToken cancellationToken,
|
||||
[FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
|
||||
+1
-3
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType.Item;
|
||||
@@ -26,8 +26,6 @@ public class SearchDataTypeItemController : DatatypeItemControllerBase
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedModel<DataTypeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
[EndpointSummary("Searches data type items.")]
|
||||
[EndpointDescription("Searches data type items by the provided query with pagination support.")]
|
||||
public async Task<IActionResult> Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100)
|
||||
{
|
||||
PagedModel<IEntitySlim> searchResult = _entitySearchService.Search(UmbracoObjectTypes.DataType, query, skip, take);
|
||||
|
||||
@@ -29,8 +29,6 @@ public class MoveDataTypeController : DataTypeControllerBase
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[EndpointSummary("Moves a data type.")]
|
||||
[EndpointDescription("Moves an existing data type identified by Id to a different container. The target container Id must be provided in the request model.")]
|
||||
public async Task<IActionResult> Move(CancellationToken cancellationToken, Guid id, MoveDataTypeRequestModel moveDataTypeRequestModel)
|
||||
{
|
||||
IDataType? source = await _dataTypeService.GetAsync(id);
|
||||
|
||||
-2
@@ -27,8 +27,6 @@ public class ReferencedByDataTypeController : DataTypeControllerBase
|
||||
[HttpGet("{id:guid}/referenced-by")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
|
||||
[EndpointSummary("Gets a paged collection of entities that are referenced by a data type.")]
|
||||
[EndpointDescription("Gets a paged collection of entities that are referenced by the data type with the provided Id, so you can see where it is being used.")]
|
||||
public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> ReferencedBy(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
|
||||
-2
@@ -26,8 +26,6 @@ public class AncestorsDataTypeTreeController : DataTypeTreeControllerBase
|
||||
[HttpGet("ancestors")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
[EndpointSummary("Gets a collection of ancestor data type folders.")]
|
||||
[EndpointDescription("Gets a collection of data type folders that are ancestors to the provided Id.")]
|
||||
public async Task<ActionResult<IEnumerable<DataTypeTreeItemResponseModel>>> Ancestors(CancellationToken cancellationToken, Guid descendantId)
|
||||
=> await GetAncestors(descendantId);
|
||||
}
|
||||
|
||||
-2
@@ -27,8 +27,6 @@ public class ChildrenDataTypeTreeController : DataTypeTreeControllerBase
|
||||
[HttpGet("children")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
[EndpointSummary("Gets a collection of data type tree child items.")]
|
||||
[EndpointDescription("Gets a paginated collection of data type tree items that are children of the provided parent Id.")]
|
||||
public async Task<ActionResult<PagedViewModel<DataTypeTreeItemResponseModel>>> Children(CancellationToken cancellationToken, Guid parentId, int skip = 0, int take = 100, bool foldersOnly = false)
|
||||
{
|
||||
RenderFoldersOnly(foldersOnly);
|
||||
|
||||
+3
-19
@@ -31,24 +31,8 @@ public class DataTypeTreeControllerBase : FolderTreeControllerBase<DataTypeTreeI
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public DataTypeTreeControllerBase(IEntityService entityService, FlagProviderCollection flagProviders, IDataTypeService dataTypeService)
|
||||
: this(
|
||||
entityService,
|
||||
flagProviders,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IEntitySearchService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IIdKeyMap>(),
|
||||
dataTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
public DataTypeTreeControllerBase(
|
||||
IEntityService entityService,
|
||||
FlagProviderCollection flagProviders,
|
||||
IEntitySearchService entitySearchService,
|
||||
IIdKeyMap idKeyMap,
|
||||
IDataTypeService dataTypeService)
|
||||
: base(entityService, flagProviders, entitySearchService, idKeyMap) =>
|
||||
: base(entityService, flagProviders) =>
|
||||
_dataTypeService = dataTypeService;
|
||||
|
||||
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.DataType;
|
||||
@@ -59,8 +43,8 @@ public class DataTypeTreeControllerBase : FolderTreeControllerBase<DataTypeTreeI
|
||||
{
|
||||
get
|
||||
{
|
||||
var ordering = Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.NodeObjectTypeColumnName, Direction.Descending); // We need to override to change direction
|
||||
ordering.Next = Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.TextColumnName);
|
||||
var ordering = Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.NodeObjectType), Direction.Descending); // We need to override to change direction
|
||||
ordering.Next = Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.Text));
|
||||
|
||||
return ordering;
|
||||
}
|
||||
|
||||
@@ -27,8 +27,6 @@ public class RootDataTypeTreeController : DataTypeTreeControllerBase
|
||||
[HttpGet("root")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
[EndpointSummary("Gets a collection of data type items from the root of the tree.")]
|
||||
[EndpointDescription("Gets a paginated collection of data type items from the root of the tree with optional filtering.")]
|
||||
public async Task<ActionResult<PagedViewModel<DataTypeTreeItemResponseModel>>> Root(CancellationToken cancellationToken, int skip = 0, int take = 100, bool foldersOnly = false)
|
||||
{
|
||||
RenderFoldersOnly(foldersOnly);
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Services.Flags;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class SearchDataTypeTreeController : DataTypeTreeControllerBase
|
||||
{
|
||||
public SearchDataTypeTreeController(
|
||||
IEntityService entityService,
|
||||
FlagProviderCollection flagProviders,
|
||||
IEntitySearchService entitySearchService,
|
||||
IIdKeyMap idKeyMap,
|
||||
IDataTypeService dataTypeService)
|
||||
: base(entityService, flagProviders, entitySearchService, idKeyMap, dataTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<DataTypeTreeItemResponseModel>>> Search(CancellationToken cancellationToken, string? query, int skip = 0, int take = 100, TreeItemKind itemKind = TreeItemKind.All)
|
||||
=> await SearchTreeEntities(query, skip, take, itemKind);
|
||||
}
|
||||
-2
@@ -24,8 +24,6 @@ public class SiblingsDataTypeTreeController : DataTypeTreeControllerBase
|
||||
|
||||
[HttpGet("siblings")]
|
||||
[ProducesResponseType(typeof(SubsetViewModel<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
[EndpointSummary("Gets a collection of data type tree sibling items.")]
|
||||
[EndpointDescription("Gets a paged collection of data type tree items that are siblings of the provided Id. The collection can be optionally filtered to return only folder, or folders and data types.")]
|
||||
public async Task<ActionResult<SubsetViewModel<DataTypeTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, bool foldersOnly = false)
|
||||
{
|
||||
RenderFoldersOnly(foldersOnly);
|
||||
|
||||
@@ -33,8 +33,6 @@ public class UpdateDataTypeController : DataTypeControllerBase
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[EndpointSummary("Updates a data type.")]
|
||||
[EndpointDescription("Updates a data type identified by the provided Id with the details from the request model.")]
|
||||
public async Task<IActionResult> Update(CancellationToken cancellationToken, Guid id, UpdateDataTypeRequestModel updateDataTypeViewModel)
|
||||
{
|
||||
IDataType? current = await _dataTypeService.GetAsync(id);
|
||||
|
||||
@@ -25,8 +25,6 @@ public class AllDictionaryController : DictionaryControllerBase
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<DictionaryOverviewResponseModel>), StatusCodes.Status200OK)]
|
||||
[EndpointSummary("Gets a paginated collection of dictionary items.")]
|
||||
[EndpointDescription("Gets a paginated collection of dictionary items with optional filtering by name.")]
|
||||
public async Task<ActionResult<PagedViewModel<DictionaryOverviewResponseModel>>> All(
|
||||
CancellationToken cancellationToken,
|
||||
string? filter = null,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user