feat(licenses): add domain restriction to packages and key generation #463
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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).",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}}>
|
||||
|
||||
Reference in New Issue
Block a user