feat(licenses): key prefix (#406), header button (#408), open feed (#409) #416

Merged
jmiller merged 1 commits from dev into main 2026-06-02 13:52:32 +00:00
8 changed files with 50 additions and 21 deletions
+15 -5
View File
@@ -48,14 +48,18 @@ func (LicenseKey) TableName() string {
return "license_key"
}
// GenerateKeyString creates a random license key in MOKO-XXXX-XXXX-XXXX-XXXX format.
func GenerateKeyString() (string, error) {
// GenerateKeyString creates a random license key in PREFIX-XXXX-XXXX-XXXX-XXXX format.
// If prefix is empty, defaults to "MOKO".
func GenerateKeyString(prefix string) (string, error) {
if prefix == "" {
prefix = "MOKO"
}
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", err
}
hex := strings.ToUpper(hex.EncodeToString(b))
return fmt.Sprintf("MOKO-%s-%s-%s-%s", hex[0:4], hex[4:8], hex[8:12], hex[12:16]), nil
h := strings.ToUpper(hex.EncodeToString(b))
return fmt.Sprintf("%s-%s-%s-%s-%s", prefix, h[0:4], h[4:8], h[8:12], h[12:16]), nil
}
// HashKey returns the SHA-256 hash of a raw key string.
@@ -65,8 +69,14 @@ func HashKey(rawKey string) string {
}
// CreateLicenseKey generates a new key, stores it in plaintext and hashed, and returns the raw key.
// The prefix is looked up from the org's update stream config.
func CreateLicenseKey(ctx context.Context, key *LicenseKey) (rawKey string, err error) {
rawKey, err = GenerateKeyString()
prefix := ""
cfg := GetEffectiveConfig(ctx, key.OwnerID, 0)
if cfg != nil && cfg.KeyPrefix != "" {
prefix = cfg.KeyPrefix
}
rawKey, err = GenerateKeyString(prefix)
if err != nil {
return "", fmt.Errorf("GenerateKeyString: %w", err)
}
+1
View File
@@ -30,6 +30,7 @@ type UpdateStreamConfig struct {
FeedVisibility string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'public'"` // public, no-download, hidden
DownloadGating string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'none'"` // none, all, prerelease
SupportURL string `xorm:"TEXT"` // wiki or external support page URL
KeyPrefix string `xorm:"VARCHAR(20)"` // org-specific license key prefix (e.g. "ACME")
// Extension metadata — used in update feed generation.
ExtensionName string `xorm:"TEXT"` // element identifier (e.g. pkg_mokowaas, com_mokowaas)
DisplayName string `xorm:"TEXT"` // human-readable name (e.g. "Package - MokoWaaS")
+1
View File
@@ -49,6 +49,7 @@ type updateStreamConfig340 struct {
InfoURL string `xorm:"TEXT"`
TargetVersion string `xorm:"TEXT"`
PHPMinimum string `xorm:"VARCHAR(20)"`
KeyPrefix string `xorm:"VARCHAR(20)"`
}
func (updateStreamConfig340) TableName() string {
+3
View File
@@ -2677,6 +2677,7 @@
"repo.licenses.feed_joomla_updates": "Joomla updates.xml",
"repo.licenses.feed_dolibarr_updates": "Dolibarr JSON",
"repo.licenses.feed_wordpress_updates": "WordPress (PUC JSON)",
"repo.licenses.open_feed": "Open in new tab",
"repo.licenses.feed_changelog_xml": "Changelog XML (Joomla)",
"repo.licenses.master_label": "Master",
"repo.licenses.unlimited": "unlimited",
@@ -2908,6 +2909,8 @@
"org.settings.streams_tag_help": "When licensing is active, release tags with prerelease suffixes must match one of the streams above (e.g. v1.0.0-rc1 matches the -rc stream).",
"org.settings.custom_streams": "Custom Stream Definitions (JSON)",
"org.settings.custom_streams_help": "JSON array of stream objects. Each needs: name, suffix, description. Example: [{\"name\":\"lts\",\"suffix\":\"-lts\",\"description\":\"Long-term support\"}]",
"org.settings.key_prefix": "License Key Prefix",
"org.settings.key_prefix_help": "Custom prefix for license keys generated in this org (e.g. ACME, CLIENT). Leave empty for default (MOKO). Max 20 chars, auto-uppercased.",
"org.settings.parent_org": "Parent Organization",
"org.settings.parent_org_none": "(none — top-level organization)",
"org.settings.parent_org_help": "Set a parent org for enterprise hierarchy. Child orgs inherit license packages and master keys from parent orgs.",
+2
View File
@@ -5,6 +5,7 @@ package org
import (
"net/http"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
@@ -47,6 +48,7 @@ func SettingsUpdateStreamsPost(ctx *context.Context) {
FeedVisibility: ctx.FormString("feed_visibility"),
DownloadGating: ctx.FormString("download_gating"),
SupportURL: ctx.FormString("support_url"),
KeyPrefix: strings.ToUpper(strings.TrimSpace(ctx.FormString("key_prefix"))),
ExtensionName: ctx.FormString("extension_name"),
DisplayName: ctx.FormString("display_name"),
Description: ctx.FormString("feed_description"),
+9 -7
View File
@@ -25,14 +25,16 @@
</div>
{{end}}
<h4 class="ui top attached header">
{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}
<details id="new-package-details">
<h4 class="ui top attached header tw-flex tw-items-center tw-justify-between">
<span>{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}</span>
{{if .IsRepoAdmin}}
<summary class="ui primary small button">{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.new_package"}}</summary>
{{end}}
</h4>
<div class="ui attached segment">
{{if .IsRepoAdmin}}
<details class="tw-mb-4">
<summary class="ui primary button">{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.new_package"}}</summary>
<div class="tw-mt-4">
<div class="tw-mb-4">
<form class="ui form" method="post" action="{{$.Org.HomeLink}}/-/licenses/packages">
{{.CsrfTokenHtml}}
<div class="two fields">
@@ -80,9 +82,9 @@
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.licenses.create_package"}}</button>
</form>
</div>
</details>
</div>
{{end}}
</details>
{{if .LicensePackages}}
<table class="ui compact table">
<thead>
@@ -37,6 +37,12 @@
<p class="help">{{ctx.Locale.Tr "org.settings.feed_visibility_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.key_prefix"}}</label>
<input name="key_prefix" value="{{.StreamConfig.KeyPrefix}}" placeholder="MOKO" maxlength="20">
<p class="help">{{ctx.Locale.Tr "org.settings.key_prefix_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.download_gating"}}</label>
<select name="download_gating" class="ui dropdown">
+13 -9
View File
@@ -26,8 +26,12 @@
{{end}}
{{/* License Packages */}}
<h4 class="ui top attached header">
{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}
<details id="new-package-details">
<h4 class="ui top attached header tw-flex tw-items-center tw-justify-between">
<span>{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}</span>
{{if .IsRepoAdmin}}
<summary class="ui primary small button">{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.new_package"}}</summary>
{{end}}
</h4>
<div class="ui attached segment">
{{if .LicensePackages}}
@@ -90,12 +94,9 @@
{{end}}
</div>
{{/* Create New License Package */}}
{{/* Create New License Package (form panel, toggled by header button) */}}
{{if .IsRepoAdmin}}
<div class="tw-mt-4">
<details>
<summary class="ui primary button">{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.new_package"}}</summary>
<div class="ui segment tw-mt-2">
<div class="ui attached segment">
<form class="ui form" method="post" action="{{.RepoLink}}/licenses/packages">
{{.CsrfTokenHtml}}
<div class="two fields">
@@ -143,9 +144,8 @@
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.licenses.create_package"}}</button>
</form>
</div>
</details>
</div>
</details>
{{end}}
{{/* Issued Keys */}}
@@ -272,6 +272,7 @@
<div class="ui action input tw-w-full">
<input class="js-feed-url-joomla" type="text" readonly value="{{.Repository.HTMLURL ctx}}/updates.xml" onclick="this.select()">
<button class="ui button" data-clipboard-target=".js-feed-url-joomla" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 14}}</button>
<a class="ui button" href="{{.Repository.HTMLURL ctx}}/updates.xml" target="_blank" data-tooltip-content="{{ctx.Locale.Tr "repo.licenses.open_feed"}}">{{svg "octicon-link-external" 14}}</a>
</div>
</div>
{{end}}
@@ -281,6 +282,7 @@
<div class="ui action input tw-w-full">
<input class="js-feed-url-dolibarr" type="text" readonly value="{{.Repository.HTMLURL ctx}}/updates/dolibarr.json" onclick="this.select()">
<button class="ui button" data-clipboard-target=".js-feed-url-dolibarr" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 14}}</button>
<a class="ui button" href="{{.Repository.HTMLURL ctx}}/updates/dolibarr.json" target="_blank" data-tooltip-content="{{ctx.Locale.Tr "repo.licenses.open_feed"}}">{{svg "octicon-link-external" 14}}</a>
</div>
</div>
{{end}}
@@ -290,6 +292,7 @@
<div class="ui action input tw-w-full">
<input class="js-feed-url-wordpress" type="text" readonly value="{{.Repository.HTMLURL ctx}}/updates/wordpress.json" onclick="this.select()">
<button class="ui button" data-clipboard-target=".js-feed-url-wordpress" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 14}}</button>
<a class="ui button" href="{{.Repository.HTMLURL ctx}}/updates/wordpress.json" target="_blank" data-tooltip-content="{{ctx.Locale.Tr "repo.licenses.open_feed"}}">{{svg "octicon-link-external" 14}}</a>
</div>
</div>
{{end}}
@@ -298,6 +301,7 @@
<div class="ui action input tw-w-full">
<input class="js-feed-url-changelog" type="text" readonly value="{{.Repository.HTMLURL ctx}}/changelog.xml" onclick="this.select()">
<button class="ui button" data-clipboard-target=".js-feed-url-changelog" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 14}}</button>
<a class="ui button" href="{{.Repository.HTMLURL ctx}}/changelog.xml" target="_blank" data-tooltip-content="{{ctx.Locale.Tr "repo.licenses.open_feed"}}">{{svg "octicon-link-external" 14}}</a>
</div>
</div>
</div>