feat(licenses): full commercial license management system v1.26.1-moko.06.02.00 #402

Merged
jmiller merged 15 commits from dev into main 2026-06-02 12:00:24 +00:00
Owner

Summary

Full commercial license management system for organizations selling Joomla, WordPress, and Dolibarr extensions. 14 commits implementing:

  • License package CRUD with archiving, search, sortable tables
  • Key management: create, edit, renew, revoke, delete with domain enforcement
  • Update feeds: Joomla XML, Dolibarr JSON, WordPress PUC JSON, Changelog XML
  • Download gating (none/prerelease/all) and feed visibility (public/no-download/hidden)
  • Domain lock-on-first-use with configurable grace period
  • RepoScope enforcement — packages scoped to specific repos
  • Full REST API: packages, keys, settings, purchase webhook, public validation
  • Extension metadata configurable at org and repo levels
  • SHA256 checksums, Joomla-standard tags, Akeeba-compatible dlid pattern
  • Migration v340 syncing all new columns
  • Integration tests for API endpoints
  • Combolist channel picker (label-style UI)
  • Double confirmation modals for permanent deletion
  • No-download mode: release notes visible, files require login

Test plan

All implemented issues have testing plans as comments. Label: pending: testing.

  • Deploy to dev environment
  • Run go test -run TestAPILicense ./tests/integration/
  • GUI testing per issue checklists
  • Verify Joomla XML feed with real Joomla instance
  • Verify download gating modes
  • Verify feed visibility modes

🤖 Generated with Claude Code

## Summary Full commercial license management system for organizations selling Joomla, WordPress, and Dolibarr extensions. 14 commits implementing: - License package CRUD with archiving, search, sortable tables - Key management: create, edit, renew, revoke, delete with domain enforcement - Update feeds: Joomla XML, Dolibarr JSON, WordPress PUC JSON, Changelog XML - Download gating (none/prerelease/all) and feed visibility (public/no-download/hidden) - Domain lock-on-first-use with configurable grace period - RepoScope enforcement — packages scoped to specific repos - Full REST API: packages, keys, settings, purchase webhook, public validation - Extension metadata configurable at org and repo levels - SHA256 checksums, Joomla-standard tags, Akeeba-compatible dlid pattern - Migration v340 syncing all new columns - Integration tests for API endpoints - Combolist channel picker (label-style UI) - Double confirmation modals for permanent deletion - No-download mode: release notes visible, files require login ## Test plan All implemented issues have testing plans as comments. Label: `pending: testing`. - [ ] Deploy to dev environment - [ ] Run `go test -run TestAPILicense ./tests/integration/` - [ ] GUI testing per issue checklists - [ ] Verify Joomla XML feed with real Joomla instance - [ ] Verify download gating modes - [ ] Verify feed visibility modes 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jmiller added 14 commits 2026-06-02 11:49:43 +00:00
Migration v340: sync all missing columns (key_raw, payment_ref,
last_heartbeat_unix, is_archived, licensing_enabled, download_gating,
support_url, and all extension metadata fields).

Package archiving (#384): add IsArchived field with archive/unarchive
handlers and collapsible "Archived Packages" section in templates.
Existing keys from archived packages continue to work.

Expanded delete permissions (#385): org owners and site admins can
permanently delete packages and keys (previously site admin only).

Search (#392): server-side search across key_prefix, key_raw,
licensee_name, licensee_email, domain_restriction, and payment_ref
via ?q= query parameter on both repo and org licenses pages.

Sortable tables (#390): Fomantic UI sortable class on keys table
with new Domain column showing DomainRestriction per key.

Download gating (#347): three modes — none, prerelease-only, and
all downloads. CheckDownloadGating() intercepts both release
attachment and git archive download handlers.

Support URL (#393): configurable SupportURL field on
UpdateStreamConfig for wiki or external site links.

Changelog XML (#343): ServeChangelogXML endpoint at /changelog.xml
generates Joomla-compatible changelog from release notes. Parses
Keep-a-Changelog markdown sections into <security>, <fix>,
<addition>, <change>, <remove>, <note> XML elements.

API renew (#387): POST /license-keys/{id}/renew endpoint extends
key expiration by package duration.

Closes #384, #385, #386, #387, #389, #390, #392, #393
Refs #343, #346, #347

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Domain lock timer: add DomainLockHours to LicensePackage and
FirstUsedUnix to LicenseKey. During the grace period after first
use, any domain is accepted and auto-added to the restriction list.
After the grace period, only listed domains are allowed. Set 0 for
immediate lock-on-first-use (default).

Fix infourl: default to /releases listing page instead of specific
tag page. Falls back to SupportURL or InfoURL if configured.

Match Akeeba Backup Pro XML format: downloadkey prefix is "dlid="
(not "&dlid="), matching how Joomla stores extra_query. Verified
against production Akeeba/JCE/AdminTools manifests via SSH.

Update migration v340 with FirstUsedUnix and DomainLockHours columns.

Add DomainLockHours field to create/edit package forms for both
repo and org levels with help text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Joomla only recognizes: dev, alpha, beta, rc, stable. Our internal
stream names use longer forms (development, release-candidate).
Add joomlaTagName() to map between conventions in the <tags><tag>
XML element.

Without this, Joomla's update channel filter ignores entries with
non-standard tag values like "release-candidate" or "development".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Read the .sha256 sidecar attachment (generated by
GenerateReleaseChecksums) and populate the <sha256> element in the
update XML. This matches the pattern used by Akeeba (sha512) and
JCE (sha256 + sha384 + sha512) for integrity verification.

Also fix zip attachment filter to skip .sha256 sidecar files when
selecting the download URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests cover:
- Package CRUD (list, create, create without name)
- Key CRUD (create, list, edit, renew, delete)
- Purchase webhook (new key, idempotent duplicate)
- Public validation (valid key, invalid key, domain check)

Tests follow existing Gitea integration test patterns using
unittest fixtures, user tokens, and DecodeJSON assertions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
API package endpoints (#388):
- PATCH /license-packages/{id} — edit package (master protected)
- DELETE /license-packages/{id} — delete package (master protected)
- POST /license-packages/{id}/archive — archive package
- POST /license-packages/{id}/unarchive — restore package

Settings API (#349):
- GET /license-settings — read licensing/update stream config
- PUT /license-settings — update config (all metadata fields)
- New LicenseSettings struct in API types

Repo scope UI (#395):
- Dropdown in create/edit package forms listing org repos
- "All repositories" default option
- RepoScope read from form in both repo and org handlers
- OrgRepos loaded via GetOrgRepositories in Licenses handlers

Refs #341, #346, #349, #388, #395

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace link-action delete buttons with show-modal pattern that opens
a Fomantic UI modal with a form. Package deletion requires typing
the package name in a required field before the submit button works.
Key deletion shows a warning modal with confirmation.

Uses Gitea's existing form-fetch-action + modal_actions_confirm
pattern, matching how repo deletion works in settings.

Both repo and org templates updated with matching modals.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SECURITY: ValidateLicenseKeyForRepo() now checks the package's
RepoScope field against the requesting repo ID. A package scoped
to repo A will reject keys when accessed from repo B's update feed.
Update server and download gating both use the new function.

Master/internal keys bypass repo scope checks.

RepoScope supports: "all" (any repo), single repo ID string,
or JSON array of repo IDs like ["1","5","12"].

Also adds POST /license-keys/{id}/revoke API endpoint that was
missing from the API but existed in web handlers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New endpoint: GET /{owner}/{repo}/updates/wordpress.json

Generates JSON compatible with the YahnisElsts plugin-update-checker
library — the standard for commercial WordPress plugin self-hosted
updates. Returns name, slug, version, download_url, homepage,
requires_php, author, sections (changelog HTML), icons, and banners.

License key validation: reads from ?license_key=, ?dlid=, or ?key=
query params (PUC sends these via addQueryArgFilter). When RequireKey
is enabled, returns minimal empty response without download_url.

Changelog section built from release notes (last 10 stable releases),
converting markdown list items to HTML <ul>/<li> elements.

Icon/banner URLs point to conventional paths in the repo:
  assets/icon-128x128.png, assets/icon-256x256.png
  assets/banner-772x250.png, assets/banner-1544x500.png

Route registered at /updates/wordpress.json alongside existing
/updates.xml (Joomla) and /updates/dolibarr.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Repo settings now include extension metadata fields (element name,
display name, type, target version, maintainer, PHP minimum) under
the Licensing section. These override org-level defaults per-repo.

Empty fields inherit from the organization's update stream config.
Extension type dropdown includes: package, component, module,
plugin, template, library — plus an "(inherit from org)" option.

Also adds form fields to the RepoSettingsForm struct for all
metadata fields.

Closes #335

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use the existing shared/combolist.tmpl component for channel
selection in create and edit package forms. This mimics the
issue label picker UI — a searchable dropdown with checkmarks
and a selected-items list below.

Replaces raw checkboxes (repo + org templates) and Fomantic
multiple selection dropdowns (edit templates) with the
combo-multiselect component that has proper JS for toggle,
search, and clear functionality.

Handler parsing updated: combolist sends comma-separated values
in a single hidden input (vs multiple checkbox form values).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add FeedVisibility field to UpdateStreamConfig with three modes:
- public: full feed with download URLs (default)
- no-download: version info visible but download URLs stripped
- hidden: empty feed returned without a valid license key

The "no-download" mode is the key commercial pattern — customers
see updates exist (motivating purchase/renewal) but cannot download
without a valid key. Joomla shows "update available" in admin.

Applied consistently across all update feed endpoints (Joomla XML,
Dolibarr JSON, WordPress JSON) via the shared validateUpdateKey()
which now returns a stripDownloads flag.

Also: when licensing is enabled, the release listing page requires
login. Anonymous users are redirected to the login page. This
prevents browsing release notes and download links without auth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In "no-download" feed visibility mode, anonymous users can browse
the release listing page and read release notes, but the download
section (attachments, source archives) is replaced with a "Sign in
to download" message.

Only "hidden" mode redirects anonymous users to login entirely.
"no-download" keeps the page public — release notes, changelogs,
and version info are all visible. Just the actual files are gated.

This matches the commercial pattern: let users see what's available
to motivate purchase, while protecting the actual deliverables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
docs: update CHANGELOG and wiki for v1.26.1-moko.06.02.00
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
PR RC Release / Build RC Release (pull_request) Successful in 19s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
913e919fba
Add changelog entry for full license management system release.
Update wiki with WordPress feed status, feed visibility modes,
and revision 1.4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
jmiller added 1 commit 2026-06-02 11:59:42 +00:00
fix(security): ownership guards, RepoScope parsing, CSRF tokens, XSS escaping
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
PR RC Release / Build RC Release (pull_request) Failing after 17s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
0e7d3c4a34
SECURITY: Add verifyPackageOwnership/verifyKeyOwnership checks to
all API handlers that accept ID parameters. Prevents cross-org
access where an admin of org A could modify org B's license data.

FIX: RepoScope validation now properly parses JSON arrays using
json.Unmarshal instead of strings.Contains. The old approach matched
substrings (repo ID "2" matched inside "12"). Now uses typed int64
comparison.

FIX: Add {{$.CsrfTokenHtml}} to both delete confirmation modal
forms (package and key) in repo and org templates. Without CSRF
tokens, the form-fetch-action POST requests would be rejected.

FIX: HTML-escape release notes in WordPress changelog to prevent
XSS via malicious release note content reaching WP admin dashboards.

FIX: Parse AllowedChannels JSON format before comma-split fallback
to avoid garbage values from splitting JSON arrays by comma.

FIX: Add missing third return value (false) on error path in
validateUpdateKey to prevent compile error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
jmiller merged commit a52ac1bf61 into main 2026-06-02 12:00:24 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: MokoConsulting/MokoGitea#402