fix(licenses): merge license UI fixes to main #462

Merged
jmiller merged 2 commits from dev into main 2026-06-04 13:49:11 +00:00
4 changed files with 133 additions and 57 deletions
+3
View File
@@ -2660,6 +2660,9 @@
"repo.licenses.key_revoked": "License key revoked.",
"repo.licenses.master_key_created": "Master License Key Created",
"repo.licenses.master_key_created_copy": "This is your organization master key with unlimited access to all update channels. Copy it now — it will not be shown again.",
"repo.licenses.regenerate_master_key": "Regenerate",
"repo.licenses.regenerate_master_key_help": "Deactivates the current master key and generates a new one. The new key will be shown once.",
"repo.licenses.master_key_regenerated": "Master key regenerated. Copy the new key below — it will not be shown again.",
"repo.licenses.update_feeds": "Update Feed URLs",
"repo.licenses.edit_key": "Edit License Key",
"repo.licenses.licensee_name": "Licensee Name",
+52
View File
@@ -90,6 +90,10 @@ func Licenses(ctx *context.Context) {
}
}
// Always load the master key for display (prefix + status).
masterKey, _ := licenses.GetMasterKey(ctx, ownerID)
ctx.Data["MasterKey"] = masterKey
pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
if err != nil {
ctx.ServerError("ListLicensePackages", err)
@@ -203,6 +207,54 @@ func LicensesCreatePackage(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
}
// LicensesRegenerateMasterKey handles POST to regenerate the master key.
func LicensesRegenerateMasterKey(ctx *context.Context) {
ownerID := ctx.Repo.Repository.OwnerID
// Deactivate the old master key.
oldKey, _ := licenses.GetMasterKey(ctx, ownerID)
if oldKey != nil {
oldKey.IsActive = false
_ = licenses.UpdateLicenseKey(ctx, oldKey)
}
// Find the master package.
pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
if err != nil {
ctx.ServerError("ListLicensePackages", err)
return
}
var masterPkg *licenses.LicensePackage
for _, pkg := range pkgs {
if pkg.Name == licenses.MasterPackageName {
masterPkg = pkg
break
}
}
if masterPkg == nil {
ctx.Flash.Error("Master package not found")
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
return
}
// Create a new master key.
newKey := &licenses.LicenseKey{
PackageID: masterPkg.ID,
OwnerID: ownerID,
IsInternal: true,
IsActive: true,
}
rawKey, err := licenses.CreateLicenseKey(ctx, newKey)
if err != nil {
ctx.ServerError("CreateLicenseKey", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.licenses.master_key_regenerated"))
ctx.Data["NewMasterKey"] = rawKey
Licenses(ctx)
}
// LicensesGenerateKey handles POST to generate a new key from a package.
func LicensesGenerateKey(ctx *context.Context) {
packageID, _ := strconv.ParseInt(ctx.FormString("package_id"), 10, 64)
+1
View File
@@ -1549,6 +1549,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Post("/packages/{id}/archive", repo.LicensesArchivePackage)
m.Post("/packages/{id}/unarchive", repo.LicensesUnarchivePackage)
m.Post("/keys/generate", repo.LicensesGenerateKey)
m.Post("/master-key/regenerate", repo.LicensesRegenerateMasterKey)
m.Get("/keys/{id}/edit", repo.LicensesEditKey)
m.Post("/keys/{id}/edit", repo.LicensesEditKeyPost)
m.Post("/keys/{id}/revoke", repo.LicensesRevokeKey)
+77 -57
View File
@@ -25,12 +25,32 @@
</div>
{{end}}
{{/* Master Key Info */}}
{{if and .MasterKey .IsRepoAdmin}}
<div class="ui segment tw-flex tw-items-center tw-justify-between tw-gap-4">
<div class="tw-flex tw-items-center tw-gap-2">
<span class="ui tiny orange label">{{ctx.Locale.Tr "repo.licenses.master_label"}}</span>
<code>{{.MasterKey.KeyPrefix}}</code>
{{if .MasterKey.IsActive}}
<span class="ui tiny green label">{{ctx.Locale.Tr "repo.licenses.active"}}</span>
{{else}}
<span class="ui tiny grey label">{{ctx.Locale.Tr "repo.licenses.inactive"}}</span>
{{end}}
</div>
<form method="post" action="{{.RepoLink}}/licenses/master-key/regenerate" class="tw-inline">
{{.CsrfTokenHtml}}
<button class="ui tiny primary button" type="submit" data-tooltip-content="{{ctx.Locale.Tr "repo.licenses.regenerate_master_key_help"}}">
{{svg "octicon-sync" 14}} {{ctx.Locale.Tr "repo.licenses.regenerate_master_key"}}
</button>
</form>
</div>
{{end}}
{{/* License 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>
<button class="ui primary small button show-modal" data-modal="#new-package-modal">{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.new_package"}}</button>
{{end}}
</h4>
<div class="ui attached segment">
@@ -94,60 +114,6 @@
{{end}}
</div>
{{/* Create New License Package (form panel, toggled by header button) */}}
{{if .IsRepoAdmin}}
<div class="ui attached segment">
<form class="ui form" method="post" action="{{.RepoLink}}/licenses/packages">
{{.CsrfTokenHtml}}
<div class="two fields">
<div class="required field">
<label>{{ctx.Locale.Tr "repo.licenses.package_name"}}</label>
<input name="name" required placeholder="e.g. Pro Annual, Basic Monthly">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.description"}}</label>
<input name="description" placeholder="e.g. Annual pro subscription with all channels">
</div>
</div>
<div class="fields">
<div class="four wide field">
<label>{{ctx.Locale.Tr "repo.licenses.duration"}} ({{ctx.Locale.Tr "repo.licenses.days"}})</label>
<input name="duration_days" type="number" value="0" min="0">
<p class="help">0 = {{ctx.Locale.Tr "repo.licenses.lifetime"}}</p>
</div>
<div class="four wide field">
<label>{{ctx.Locale.Tr "repo.licenses.max_sites"}}</label>
<input name="max_sites" type="number" value="0" min="0">
<p class="help">0 = {{ctx.Locale.Tr "repo.licenses.unlimited"}}</p>
</div>
<div class="four wide field">
<label>{{ctx.Locale.Tr "repo.licenses.domain_lock_hours"}}</label>
<input name="domain_lock_hours" type="number" value="0" min="0">
<p class="help">{{ctx.Locale.Tr "repo.licenses.domain_lock_hours_help"}}</p>
</div>
<div class="field">
{{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>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.repo_scope"}}</label>
<select name="repo_scope" class="ui dropdown">
<option value="all">{{ctx.Locale.Tr "repo.licenses.repo_scope_all"}}</option>
{{if .OrgRepos}}
{{range .OrgRepos}}
<option value="{{.ID}}">{{.Name}}</option>
{{end}}
{{end}}
</select>
<p class="help">{{ctx.Locale.Tr "repo.licenses.repo_scope_help"}}</p>
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.licenses.create_package"}}</button>
</form>
</div>
</details>
{{end}}
{{/* Issued Keys */}}
{{if or .LicenseKeys .SearchQuery}}
<h4 class="ui top attached header tw-mt-4">
@@ -183,7 +149,7 @@
<td>
<div class="tw-flex tw-items-center tw-gap-1">
<code class="js-license-key-{{.ID}}">{{if .KeyRaw}}{{.KeyRaw}}{{else}}{{.KeyPrefix}}{{end}}</code>
{{if .KeyRaw}}<button class="ui tiny icon button" data-clipboard-target=".js-license-key-{{.ID}}" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 12}}</button>{{end}}
<button class="ui tiny icon button" data-clipboard-text="{{if .KeyRaw}}{{.KeyRaw}}{{else}}{{.KeyPrefix}}{{end}}" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 12}}</button>
{{if .IsInternal}} <span class="ui tiny orange label">{{ctx.Locale.Tr "repo.licenses.master_label"}}</span>{{end}}
</div>
</td>
@@ -309,6 +275,60 @@
</div>
</div>
{{/* Create New License Package Modal */}}
{{if .IsRepoAdmin}}
<div class="ui small modal" id="new-package-modal">
<div class="header">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.licenses.new_package"}}</div>
<div class="content">
<form class="ui form" method="post" action="{{.RepoLink}}/licenses/packages">
{{.CsrfTokenHtml}}
<div class="required field">
<label>{{ctx.Locale.Tr "repo.licenses.package_name"}}</label>
<input name="name" required placeholder="e.g. Pro Annual, Basic Monthly">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.description"}}</label>
<input name="description" placeholder="e.g. Annual pro subscription with all channels">
</div>
<div class="three fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.duration"}} ({{ctx.Locale.Tr "repo.licenses.days"}})</label>
<input name="duration_days" type="number" value="0" min="0">
<p class="help">0 = {{ctx.Locale.Tr "repo.licenses.lifetime"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.max_sites"}}</label>
<input name="max_sites" type="number" value="0" min="0">
<p class="help">0 = {{ctx.Locale.Tr "repo.licenses.unlimited"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.domain_lock_hours"}}</label>
<input name="domain_lock_hours" type="number" value="0" min="0">
<p class="help">{{ctx.Locale.Tr "repo.licenses.domain_lock_hours_help"}}</p>
</div>
</div>
<div class="field">
{{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.repo_scope"}}</label>
<select name="repo_scope" class="ui dropdown">
<option value="all">{{ctx.Locale.Tr "repo.licenses.repo_scope_all"}}</option>
{{if .OrgRepos}}
{{range .OrgRepos}}
<option value="{{.ID}}">{{.Name}}</option>
{{end}}
{{end}}
</select>
<p class="help">{{ctx.Locale.Tr "repo.licenses.repo_scope_help"}}</p>
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.licenses.create_package"))}}
</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>