diff --git a/models/licenses/license_key.go b/models/licenses/license_key.go index 5344e660a0..9e261bd9bd 100644 --- a/models/licenses/license_key.go +++ b/models/licenses/license_key.go @@ -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) } diff --git a/models/licenses/update_stream_config.go b/models/licenses/update_stream_config.go index ed63f52467..9f465a21de 100644 --- a/models/licenses/update_stream_config.go +++ b/models/licenses/update_stream_config.go @@ -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") diff --git a/models/migrations/v1_27/v340.go b/models/migrations/v1_27/v340.go index 3dc3df6e38..8c70fd996b 100644 --- a/models/migrations/v1_27/v340.go +++ b/models/migrations/v1_27/v340.go @@ -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 { diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 323dc721c1..a6c34827b6 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -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.", diff --git a/routers/web/org/update_streams.go b/routers/web/org/update_streams.go index fcf3de0876..0b32b1359c 100644 --- a/routers/web/org/update_streams.go +++ b/routers/web/org/update_streams.go @@ -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"), diff --git a/templates/org/licenses.tmpl b/templates/org/licenses.tmpl index ef0ae1da53..e5d53501d7 100644 --- a/templates/org/licenses.tmpl +++ b/templates/org/licenses.tmpl @@ -25,14 +25,16 @@ {{end}} -