feat(licenses): add domain restriction to packages and key generation #463

Merged
jmiller merged 1 commits from feat/license-domain-restriction into dev 2026-06-04 14:07:02 +00:00
7 changed files with 97 additions and 23 deletions
+4
View File
@@ -32,6 +32,10 @@ type LicensePackage struct {
// AllowedChannels defines which update streams keys from this package
// can access. JSON array, e.g. ["stable","rc"]. Empty = all channels.
AllowedChannels string `xorm:"TEXT"`
// DomainRestriction is a comma-separated list of allowed domains.
// Keys generated from this package inherit this unless overridden.
// Empty = no restriction.
DomainRestriction string `xorm:"TEXT"`
IsActive bool `xorm:"NOT NULL DEFAULT true"`
IsArchived bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
+1
View File
@@ -421,6 +421,7 @@ func prepareMigrationTasks() []*migration {
newMigration(341, "Add parent_org_id to user table for enterprise sub-org hierarchy", v1_27.AddParentOrgIDToUser),
newMigration(342, "Add is_hidden to repository for three-level visibility", v1_27.AddIsHiddenToRepository),
newMigration(343, "Add custom field tables for issue custom fields", v1_27.AddCustomFieldTables),
newMigration(344, "Add domain_restriction to license_package table", v1_27.AddDomainRestrictionToLicensePackage),
}
return preparedMigrations
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"xorm.io/xorm"
)
func AddDomainRestrictionToLicensePackage(x *xorm.Engine) error {
type LicensePackage struct {
DomainRestriction string `xorm:"TEXT"`
}
return x.Sync(new(LicensePackage))
}
+2 -1
View File
@@ -2668,7 +2668,8 @@
"repo.licenses.licensee_name": "Licensee Name",
"repo.licenses.licensee_email": "Licensee Email",
"repo.licenses.domain_restriction": "Domain Restriction",
"repo.licenses.domain_restriction_help": "Comma-separated list of allowed domains. Leave empty for no restriction.",
"repo.licenses.domain_restriction_help": "Comma-separated list of allowed domains. Leave empty to inherit from the package default.",
"repo.licenses.domain_restriction_package_help": "Default domain restriction for keys generated from this package. Comma-separated. Keys can override this.",
"repo.licenses.use_package_default": "use package default",
"repo.licenses.expires_at": "Expires At",
"repo.licenses.expires_at_help": "Leave empty for no expiry (lifetime).",
+23 -12
View File
@@ -187,15 +187,16 @@ func LicensesCreatePackage(ctx *context.Context) {
}
pkg := &licenses.LicensePackage{
OwnerID: ctx.Repo.Repository.OwnerID,
Name: name,
Description: ctx.FormString("description"),
DurationDays: durationDays,
MaxSites: maxSites,
DomainLockHours: domainLockHours,
AllowedChannels: allowedChannels,
RepoScope: repoScope,
IsActive: true,
OwnerID: ctx.Repo.Repository.OwnerID,
Name: name,
Description: ctx.FormString("description"),
DurationDays: durationDays,
MaxSites: maxSites,
DomainLockHours: domainLockHours,
AllowedChannels: allowedChannels,
DomainRestriction: strings.TrimSpace(ctx.FormString("domain_restriction")),
RepoScope: repoScope,
IsActive: true,
}
if err := licenses.CreateLicensePackage(ctx, pkg); err != nil {
@@ -270,10 +271,19 @@ func LicensesGenerateKey(ctx *context.Context) {
return
}
// Domain restriction: use form input, fall back to package default.
domainRestriction := strings.TrimSpace(ctx.FormString("domain_restriction"))
if domainRestriction == "" && pkg.DomainRestriction != "" {
domainRestriction = pkg.DomainRestriction
}
key := &licenses.LicenseKey{
PackageID: packageID,
OwnerID: ctx.Repo.Repository.OwnerID,
IsActive: true,
PackageID: packageID,
OwnerID: ctx.Repo.Repository.OwnerID,
IsActive: true,
DomainRestriction: domainRestriction,
LicenseeName: strings.TrimSpace(ctx.FormString("licensee_name")),
LicenseeEmail: strings.TrimSpace(ctx.FormString("licensee_email")),
}
// Auto-calculate expiry from package duration.
@@ -500,6 +510,7 @@ func LicensesEditPackagePost(ctx *context.Context) {
pkg.AllowedChannels = ""
}
pkg.DomainRestriction = strings.TrimSpace(ctx.FormString("domain_restriction"))
pkg.IsActive = ctx.FormString("is_active") == "on"
if err := licenses.UpdateLicensePackage(ctx, pkg); err != nil {
+47 -10
View File
@@ -76,16 +76,14 @@
<td>{{if .IsActive}}<span class="ui green label">{{ctx.Locale.Tr "repo.licenses.active"}}</span>{{else}}<span class="ui grey label">{{ctx.Locale.Tr "repo.licenses.inactive"}}</span>{{end}}</td>
{{if $.IsRepoAdmin}}
<td class="tw-text-right tw-flex tw-gap-1 tw-justify-end">
<form method="post" action="{{$.RepoLink}}/licenses/keys/generate" class="tw-inline tw-flex tw-gap-1 tw-items-center">
{{$.CsrfTokenHtml}}
<input type="hidden" name="package_id" value="{{.ID}}">
{{if $.IsSiteAdmin}}
<input type="text" name="custom_key" placeholder="{{ctx.Locale.Tr "repo.licenses.custom_key_placeholder"}}" class="tw-w-32 tw-text-xs" title="{{ctx.Locale.Tr "repo.licenses.custom_key_help"}}">
{{end}}
<button class="ui tiny primary button" type="submit" title="{{ctx.Locale.Tr "repo.licenses.generate_key"}}">
{{svg "octicon-plus" 14}}
</button>
</form>
<button class="ui tiny primary button show-modal"
data-modal="#generate-key-modal"
data-modal-generate-key-modal-package-id="{{.ID}}"
data-modal-generate-key-modal-package-name="{{.Name}}"
data-modal-generate-key-modal-package-domain="{{.DomainRestriction}}"
title="{{ctx.Locale.Tr "repo.licenses.generate_key"}}">
{{svg "octicon-plus" 14}}
</button>
{{if ne .Name "Master (Internal)"}}
<a class="ui tiny button" href="{{$.RepoLink}}/licenses/packages/{{.ID}}/edit" title="{{ctx.Locale.Tr "repo.licenses.edit_package"}}">
{{svg "octicon-pencil" 14}}
@@ -311,6 +309,11 @@
{{template "shared/combolist" dict "Name" "allowed_channels" "Title" (ctx.Locale.Tr "repo.licenses.channels") "Items" $.ChannelItems "SelectedValues" "" "EmptyText" (ctx.Locale.Tr "repo.licenses.all_channels") "Icon" "octicon-tag"}}
<p class="help">{{ctx.Locale.Tr "repo.licenses.channels_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.domain_restriction"}}</label>
<input name="domain_restriction" placeholder="example.com, example.org">
<p class="help">{{ctx.Locale.Tr "repo.licenses.domain_restriction_package_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.repo_scope"}}</label>
<select name="repo_scope" class="ui dropdown">
@@ -329,6 +332,40 @@
</div>
{{end}}
{{/* Generate Key Modal */}}
{{if .IsRepoAdmin}}
<div class="ui small modal" id="generate-key-modal">
<div class="header">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.licenses.generate_key"}}</div>
<div class="content">
<form class="ui form" method="post" action="{{.RepoLink}}/licenses/keys/generate">
{{.CsrfTokenHtml}}
<input type="hidden" name="package_id" value="">
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.licensee_name"}}</label>
<input name="licensee_name" placeholder="Customer name">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.licensee_email"}}</label>
<input name="licensee_email" type="email" placeholder="customer@example.com">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.domain_restriction"}}</label>
<input name="domain_restriction" placeholder="example.com, example.org">
<p class="help">{{ctx.Locale.Tr "repo.licenses.domain_restriction_help"}}</p>
</div>
{{if .IsSiteAdmin}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.custom_key_placeholder"}}</label>
<input name="custom_key" placeholder="MOKO-XXXX-XXXX-XXXX">
<p class="help">{{ctx.Locale.Tr "repo.licenses.custom_key_help"}}</p>
</div>
{{end}}
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.licenses.generate_key"))}}
</form>
</div>
</div>
{{end}}
{{/* Delete Package Confirmation Modal */}}
<div class="ui small modal" id="license-delete-package-modal">
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.licenses.delete_package"}}</div>
@@ -39,6 +39,11 @@
<p class="help">{{ctx.Locale.Tr "repo.licenses.channels_help"}}</p>
</div>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.domain_restriction"}}</label>
<input name="domain_restriction" value="{{.Package.DomainRestriction}}" placeholder="example.com, example.org">
<p class="help">{{ctx.Locale.Tr "repo.licenses.domain_restriction_package_help"}}</p>
</div>
<div class="field">
<div class="ui checkbox">
<input name="is_active" type="checkbox" {{if .Package.IsActive}}checked{{end}}>