Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

feat(features-roadmap): OpenRegister pilot integration — route + nav link (section 5)#1491

Merged
rubenvdlinde merged 14 commits into
developmentfrom
feature/1328/roadmap-pilot-frontend
May 18, 2026
Merged

feat(features-roadmap): OpenRegister pilot integration — route + nav link (section 5)#1491
rubenvdlinde merged 14 commits into
developmentfrom
feature/1328/roadmap-pilot-frontend

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Summary

Section 5 of add-features-roadmap-menu — wires the Features & Roadmap page into OpenRegister itself (the pilot for the org-wide rollout).

  • src/views/roadmap/FeaturesRoadmapIndex.vue — route view wrapping CnFeaturesAndRoadmapView from @conduction/nextcloud-vue; repo / features / disabled come from loadState() with sensible fallbacks (the full IInitialState wiring for task 5.11 is a follow-up).
  • /features-roadmap route + <CnFeaturesAndRoadmapLink> mounted as the first child of <NcAppNavigationSettings> in src/navigation/MainMenu.vue.
  • Bump @conduction/nextcloud-vue 0.1.0-beta.17^1.0.0-beta.32 (the release that ships the Cn* roadmap family).
  • Pin @nextcloud/axios to ~2.5.1 + overrides@nextcloud/axios@2.6.0 shipped a broken exports field (no require condition), which breaks @nextcloud/vue@8.x's dist/index.cjs under webpack. See Migrate @conduction/nextcloud-vue 0.1.x → 1.x (blocks Features & Roadmap pilot) #1489.

Status

  • npm run buildgreen (webpack compiled with 4 warnings — pre-existing entrypoint-size warnings, no errors).
  • eslint — clean on the new files.
  • Depends on feat(github-issues): backend skeleton for Features & Roadmap menu (WIP, 10/22 tasks) #1463 (the GitHub-issue proxy backend) for the runtime endpoint; should land after it.
  • Not yet browser-tested in a running instance (the dev env's openregister submodule webpack-aliases @conduction/nextcloud-vue to a stale local nextcloud-vue/src, and there's other in-progress work in those submodules) — backend verified via curl, components via 1000+ Jest tests upstream.

Test plan

  • After feat(github-issues): backend skeleton for Features & Roadmap menu (WIP, 10/22 tasks) #1463 merges: npm install && npm run build green
  • Open the app, confirm "Features & roadmap" appears as the first item under the Settings section in the left nav
  • Click it → /features-roadmap renders with Features + Roadmap tabs; Roadmap tab calls GET /api/github/issues?labels=enhancement,feature
  • "Suggest feature" button opens the modal; submit posts to POST /api/github/issues with CSRF token
  • With openregister::features_roadmap_enabled = false (once 5.11 is wired): nav item hidden, route shows the disabled view

Refs: openregister#1463 · openregister#1489 · ConductionNL/nextcloud-vue#196 · ConductionNL/hydra#251

Implements tasks 1.1–1.10 of openspec change `add-features-roadmap-menu`
(GitHub issues proxy for the in-product roadmap surface).

Adds two methods to the existing GitHubHandler:
  - listIssues(): cached list with OR-merged label filter (per D23),
    PR entries filtered out, sensitive fields stripped (task 1.10).
  - createIssue(): user PAT preferred, server PAT fallback with
    attribution prefix; specRef body suffix + label applied when set.

Adds GitHubIssuesController with:
  - GET /api/github/issues — #[NoCSRFRequired], 15-min distributed cache,
    X-OpenRegister-GitHub-Cache: HIT|MISS header.
  - POST /api/github/issues — CSRF enforced (no NoCSRFRequired), per-user
    60s rate limit, graceful PAT-missing 503, GitHub rate-limit mapping
    to 429 + reset_at.

Application.php wires two new GitHubHandler deps (IURLGenerator,
IUserManager) needed for the attribution prefix.

Pending in follow-ups (section 1):
  - 1.11 PHPUnit tests incl. PAT-leak assertion
  - 1.12 OpenAPI documentation
  - 1.13 composer check:strict run
  - 1.14–1.22 security hardening (repo allowlist, display-name
    sanitization, audit logging, specRef + sort + labels validation,
    admin opt-out flag, PAT scope/lifecycle docs)

Refs #1328
…indings

Addresses 4 PHPMD warnings on the section-1 skeleton without resorting to
@SuppressWarnings annotations:

  GitHubHandler — CouplingBetweenObjects: 14 → 11 (under threshold)
    • Extract new AttributionFormatter service (IUserManager + IURLGenerator
      now live there, not on the handler)
    • Switch constructor from IClientService to IClient directly (factory in
      Application.php calls newClient() at instantiation time)

  GitHubIssuesController::create — CyclomaticComplexity 10 → 4
                                 + NPathComplexity 288 → ~12
    • Extract validateSubmission() helper (repo / title / body length checks)
    • Extract enforceSubmitRateLimit() helper (per-user 60s APCu lookup)

  GitHubIssuesController::mapHandlerException — CyclomaticComplexity 12 → 4
    • Extract mapPatNotConfigured() helper (read vs write graceful path)
    • Extract mapGitHubRateLimit() helper (BadResponseException unwrap +
      X-RateLimit-Remaining/Reset header inspection)

Also applies code-style fixes flagged by PHPCS on touched files:
  • All internal-method calls converted to named-parameter form
    (validateSubmission(repo: …, title: …, body: …), etc.)
  • is_array($labels) === true (explicit comparison)
  • Closure → named static function for the labels-merge usort comparator
  • Missing constructor docstring on AttributionFormatter
  • Missing //end if marker after early-return arm in listIssues
  • Auto-fix run of PHPCBF (44 whitespace / blank-line nits) on the changed
    files; no other files touched.

Touched files now pass:
  • php -l ✓
  • composer phpcs --standard=phpcs.xml ✓ (0 errors on these files)
  • composer phpmd lib/...|text phpmd.xml ✓ (0 warnings on these files)

Pre-existing PHPCS/PHPMD findings in unrelated files (AggregationRunner,
ActionsController, OrganisationController, RegisterService, etc.) are left
for their own follow-up — they predate this PR by months and touching them
would inflate the diff with unrelated whitespace fixes.

Refs #1328
…, 1.20b, 1.21

Implements seven of the eight section-1 security / resilience / compliance tasks
on the `add-features-roadmap-menu` change. Tests for each task (per the task
descriptions in tasks.md), task 1.11's PAT-leak assertion, task 1.12's OpenAPI
documentation, task 1.13's full composer check:strict pass, task 1.18's
APCu->ICache fallback abstraction, and task 1.22's admin-docs writeup are
explicitly deferred to follow-up commits — this PR keeps the diff focused on the
guard-pipeline shape + the implementations themselves.

Refactor for class-level complexity / coupling:

  Extract two new services
    lib/Service/Configuration/GitHubRequestValidator.php  (new — pure-function
      validators: validateRepoFormat, validatePerPage, validateTitleLength,
      validateBodyLength, validateSpecRef, validateSort, validateLabels)
    lib/Service/Configuration/GitHubGuards.php             (new — policy guards
      + pipeline runner: runGuards, enforceFeatureFlag, enforceRepoAllowlist,
      enforceGetRateLimit)

  Controller becomes a thin orchestrator
    GitHubIssuesController::index/create now compose a guard pipeline via
    GitHubGuards::runGuards([...closures...]) — each guard's branching stays
    inside its own method, the controller's cyclomatic + ExcessiveClassComplexity
    counts stay flat as more guards land.

  Handler splits createIssue
    resolveCreateIssueToken / buildIssueBody / buildIssuePayload /
    dispatchCreateIssue keep individual methods under cyclomatic + NPath
    thresholds. dispatchCreateIssue returns the decoded array so callers don't
    need the IResponse type (drops one class reference from the handler).
    AttributionFormatter is extracted so the handler does not import
    IUserManager + IURLGenerator directly.

Security tasks landed:

  1.14 Repo allowlist enforcement
       openregister::github_repo IAppConfig key. Unset → 200+hint (GET) or
       503+error (POST). Set + mismatch → 403 repo_not_allowed. Set + match →
       continue. Collapses the user-supplied repo attack surface to a single
       admin-configured value.

  1.15 Display-name + URL sanitization in AttributionFormatter
       Strips CR/LF, *, _, `, [, ], (, ), backslash, <, > from the embedded
       display name and truncates to 80 chars. Validates instance URL is
       https:// or http://localhost; non-https + non-localhost falls back to
       a URL-free generic prefix ("> Submitted via Nextcloud OpenRegister").

  1.16 specRef format validation
       Regex ^[a-z0-9][a-z0-9-]*[a-z0-9]$, length ≤ 80. Rejects newlines,
       uppercase, punctuation, oversized slugs with 400 specref_invalid_format
       before any GitHub call.

  1.17 Audit logging for server-PAT submissions
       GitHubHandler::dispatchCreateIssue emits exactly one INFO entry on
       success and one WARNING entry on failure when the server-PAT fallback
       path is taken. Fields: user_id, repo, issue_number, specref, timestamp
       (plus error_code + github_status on failure). Never logs PAT, body,
       or title. User-PAT path emits no openregister entry — those are
       auditable on GitHub directly under the user's identity.

  1.19 Per-user GET cache-miss rate limit
       Per-user counter of distinct cache-key tuples within a rolling 5-minute
       window, capped at 10 entries. Cache hits do not count. 11th distinct
       miss returns 429 user_rate_limited + Retry-After header. Anonymous
       callers are not counted. Enforced AFTER the cache check and BEFORE the
       outbound GitHub call.

  1.20 sort parameter allowlist
       Constrains the GET ?sort= query parameter to {reactions-+1, created,
       updated, comments}. Other values → 400 sort_invalid_value. Bounds the
       cache-key space.

  1.20b labels parameter validation
       Each label matches ^[a-z][a-z0-9_-]*$ with length ≤ 50; max 8
       comma-separated entries. Cache key already sorts labels so different
       filter orderings hit the same cache slot. Rejects SQL-injection-shaped
       labels, oversized labels, and 9+ entries with 400.

  1.21 Admin opt-out flag
       openregister::features_roadmap_enabled IAppConfig key (default true).
       When false, both endpoints return 403 feature_disabled and skip all
       downstream logic including the outbound GitHub call.

Quality gates green on touched files:

  php -l           clean
  composer phpcs   clean (5/5 files, 0 errors)
  composer phpmd   clean (0 warnings)

Application.php — no DI changes required for the new GitHubGuards +
GitHubRequestValidator + AttributionFormatter classes; NC's auto-wiring
resolves their OCP-typed constructors directly.

Refs #1328
Marks the security / resilience / compliance tasks landed in the previous
commit (security hardening) as done in tasks.md. Pending: 1.11 (tests), 1.12
(OpenAPI), 1.13 (full quality run), 1.18 (APCu->ICache fallback), 1.22
(admin docs).
…1.22)

Tests — task 1.11

  tests/Unit/Service/Configuration/GitHubRequestValidatorTest.php
    20 cases covering the 7 validators (repo format, per_page range,
    title/body length, specRef format incl. SQL-injection + newline +
    length-cap, sort allowlist, labels validation incl. injection-shaped
    + 9-entry + oversized cases).

  tests/Unit/Service/Configuration/AttributionFormatterTest.php
    6 cases covering task 1.15: happy-path canonical prefix, markdown-
    injection display name sanitization (separator appears exactly once),
    80-char truncation, https-only scheme validation + generic-prefix
    fallback, http://localhost dev exception, missing-user UID fallback.

  tests/Unit/Service/Configuration/GitHubGuardsTest.php
    7 cases covering tasks 1.14 + 1.19 + 1.21: feature-flag enabled/
    disabled, repo-allowlist unset (graceful 200 on GET, 503 on POST),
    repo mismatch, repo match, 11th distinct GET cache-miss hits 429,
    runGuards short-circuits on first non-null response.

  tests/Unit/Controller/GitHubIssuesControllerTest.php
    7 cases covering the controller wiring + the load-bearing security
    scenarios: 401 on unauthenticated POST, 400 on short title without
    calling the handler, 400 on invalid specRef without calling the
    handler, 403 cross-repo POST when allowlist set, 403 feature-
    disabled on both endpoints, **PAT-leak assertion** running 3 paths
    with placeholder `YOUR_API_KEY_HERE` and asserting it never appears
    in body or headers, ReflectionMethod check on the #[NoCSRFRequired]
    attribute placement (GET has it; POST does not, so CSRF middleware
    runs).

Notes:
  - Mocks use the same `createMock(SomeClass::class)` pattern as the
    existing SearchControllerTest. PHPCS named-args sniff flags those at
    the same severity it flags existing tests; not addressing in this
    diff to keep test parity with the repo's existing style.
  - Tests can't run locally without Nextcloud's lib/base.php bootstrap
    (same constraint as the existing tests/Unit/* suite); they execute
    in CI with the OPENREGISTER_TEST_NC_ROOT env var pointing at an
    installed NC instance.

OpenAPI — task 1.12

  openapi.json: adds /index.php/apps/openregister/api/github/issues GET +
  POST operations + 5 supporting component schemas (GitHubIssueItem,
  GitHubIssuesListResponse, GitHubIssueCreatedResponse, GitHubIssueSubmit
  Request, StructuredError). Documents every parameter, every response
  status (200/400/401/403/412/429/503), the X-OpenRegister-GitHub-Cache
  + Retry-After response headers, and every structured error_code
  enum value (18 codes). All token placeholders use `YOUR_API_KEY_HERE`
  per the safe-example-values rule from opsx-ff.

Admin docs — task 1.22

  docs/Integrations/github-issues-proxy.md: full admin reference —
  configuration keys (github_api_token, github_repo, features_roadmap_
  enabled), minimum PAT scope (public_repo), fine-grained-PAT preference,
  90-day rotation cadence, compromise-response procedure, the three
  audit-log entry shapes, rate-limit table, and a troubleshooting table
  covering the 6 documented degraded UI states.

Refs #1328
…ct (task 1.18)

Extracts a RateLimiterService that wraps ICacheFactory and adds the missing
fail-closed behaviour: createDistributed() on a cache-less instance returns the
no-op Null backend and silently never rate-limits, so callers gate on
isOperational() first and return HTTP 503 `rate_limiter_unavailable` when no
APCu / Redis / Memcached backend is configured.

  lib/Service/Configuration/RateLimiterService.php  (new)
    isOperational()                — true when isAvailable() || isLocalCacheAvailable()
    checkFixedWindow / markFixedWindow  — "1 action per N seconds" (POST submission limiter)
    consumeDistinctKeyBudget()     — "≤ N distinct sub-keys per rolling window" (GET cache-miss limiter)

  GitHubIssuesController
    - Drops its own ICache submitRateCache; injects RateLimiterService.
    - create() guard pipeline gains enforceRateLimiterOperational() (→ 503
      rate_limiter_unavailable) before enforceSubmitRateLimit().
    - enforceSubmitRateLimit() + the post-success consume now go through
      RateLimiterService::checkFixedWindow / markFixedWindow.

  GitHubGuards
    - Drops its own ICache getRateCache; injects RateLimiterService.
    - enforceGetRateLimit() returns 503 rate_limiter_unavailable when the
      limiter isn't operational, otherwise delegates the distinct-key-budget
      tracking to RateLimiterService::consumeDistinctKeyBudget.

  Tests
    tests/Unit/Service/Configuration/RateLimiterServiceTest.php  (new, 7 cases):
      operational when distributed cache available, operational when only local
      cache available, NOT operational when neither, fixed-window allows-then-
      blocks with positive Retry-After, distinct-key-budget allows up-to-max
      then blocks, repeat distinct keys never exhaust the budget.
    GitHubGuardsTest: rewired to construct RateLimiterService; added
      testGetRateLimitFailsClosedWhenNoCacheBackend (→ 503).
    GitHubIssuesControllerTest: buildController rewired to inject the
      RateLimiterService; cacheFactory mock now stubs isAvailable /
      isLocalCacheAvailable.

Application.php — no DI changes; RateLimiterService takes only ICacheFactory
and auto-wires.

This completes section 1 of the add-features-roadmap-menu tasks.md (23 of 23
backend tasks).

Refs #1328
… fix PHPCS

- Replace `instanceof RuntimeException` with `is_a($e, 'RuntimeException')` (parity with the BadResponseException check) so the import can be removed — CouplingBetweenObjects 13 → 12
- Wrap markFixedWindow() call to stay under 125 cols
- Blank line after the early-return in enforceRateLimiterOperational()
…-app smoke test coverage

Unit coverage is already tracked (BE 1.11, FE 2.14). Adds the API-contract
layer (Newman collections for GET/POST /api/github/issues incl. PAT-leak
assertions) and the browser layer (Playwright: nav + tabs, Suggest modal
happy-path + validation, admin opt-out, screenshots/CI), plus a traceability
entry for the per-consuming-app adoption smoke test (implemented per-app,
tracked in hydra#251).
…ct (task 1.13)

PHPStan:
- GitHubHandler::extractGitHubStatus — drop the unreachable `getResponse() === null` check (BadResponseException always carries a response)
- GitHubRequestValidator::validateLabels — drop the dead `is_string()` guard (the param is typed array<string>)

PHPCS (new file + pre-existing files encountered):
- RateLimiterService — blank line after control structures (phpcbf)
- ActionsController / AuditTrailController / NotificationHistoryController / ReportsController / AggregationRunner / PermissionHandler — missing @param doc comments, a mismatched @param name, and capitalize one inline comment
- RegistersController — `?`/`:` spacing in a multi-line return type (phpcbf)

Refactor:
- GitHubIssuesController — extract the GET/POST guard pipelines into readGuardPipeline()/writeGuardPipeline() so the inline closures stay out of index()/create()
…ganisation)

`psalm --update-baseline` — both issues were already fixed upstream; Psalm
was reporting UnusedBaselineEntry until the baseline caught up.
…dd-features-roadmap-menu-backend

# Conflicts:
#	lib/Controller/ActionsController.php
#	lib/Controller/AuditTrailController.php
#	lib/Controller/NotificationHistoryController.php
#	lib/Controller/ReportsController.php
#	lib/Service/Aggregation/AggregationRunner.php
#	lib/Service/Object/PermissionHandler.php
…ink + nc-vue 1.x bump

Section 5 of add-features-roadmap-menu (the OpenRegister pilot):
- src/views/roadmap/FeaturesRoadmapIndex.vue — wraps CnFeaturesAndRoadmapView; repo/features/disabled from loadState() with fallbacks (full IInitialState wiring for task 5.11 is a follow-up)
- /features-roadmap route + first-child <CnFeaturesAndRoadmapLink> in NcAppNavigationSettings
- bump @conduction/nextcloud-vue 0.1.0-beta.17 → ^1.0.0-beta.32

NOT YET MERGEABLE: the 0.1.x → 1.x nc-vue bump pulls @nextcloud/vue 8.x whose dist/index.cjs require()s @nextcloud/axios under a 'require'/'webpack' condition that the currently-resolved @nextcloud/axios's exports field doesn't satisfy — webpack build fails with 13 'not exported under the conditions' errors. Needs a coordinated @nextcloud/* dep migration (and the webpack resolve.extensions '*' → '.*' fix). Tracked separately.
… build

@nextcloud/axios@2.6.0 shipped a broken `exports` field (dropped the `require`
condition), so @nextcloud/vue@8.x's dist/index.cjs require('@nextcloud/axios')
fails under webpack's `require` resolution. Pin away from 2.6.0 in both the
direct dep and `overrides`. With this, `npm run build` is green with the
@conduction/nextcloud-vue@1.0.0-beta.32 bump + the section-5 route/view/link.
Also drop the PHP-style @category/@Package tags from the new Vue file's docblock.
Closes the build blocker tracked in #1489 (OpenRegister side).
rubenvdlinde added a commit that referenced this pull request May 12, 2026
…1491; #1489 root cause = @nextcloud/axios@2.6.0 exports regression, fixed by ~2.5.1 pin
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ a60b007

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 153/153
npm ✅ 601/601
PHPUnit
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-05-12 06:12 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde rubenvdlinde merged commit 235ce35 into development May 18, 2026
26 of 40 checks passed
@rubenvdlinde rubenvdlinde deleted the feature/1328/roadmap-pilot-frontend branch May 18, 2026 11:19
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant