feat(licenses): UI/UX cleanup, permissions, renew, auto-domain, custom keys
- Replace confirm() with Gitea modal system (link-action + data-modal-confirm) - Add confirmation modal to revoke key action - Fix clipboard copy to use data-clipboard-target with tooltip feedback - Localize all hardcoded English strings (feed labels, "unlimited", "Master") - Improve key creation flash with security-focused message + copy button - Add count badge to org licenses nav tab - Add icon to org settings navbar for update streams - Add help text to "Active" checkboxes explaining deactivation impact - Fix empty state message to reference UI creation (not just API) - Compact tables for denser license data display - Add orange "Master" label to master package rows - Conditional feed buttons on release page (only when licensing enabled) - Add TypeLicenses unit type with Read/Write/Admin team permissions - Route-level permission enforcement via RequireUnitReader/Writer - Add "Renew" action for license keys (extends by package duration) - Auto-associate domain on first heartbeat (lock-on-first-use) - Enforce max_sites limit during domain auto-association - Allow site admins and org owners to set custom license key values Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,19 @@ func CreateLicenseKey(ctx context.Context, key *LicenseKey) (rawKey string, err
|
||||
return rawKey, nil
|
||||
}
|
||||
|
||||
// CreateLicenseKeyCustom stores a key with a user-provided raw key string.
|
||||
// The raw key is hashed and stored — it will not be recoverable after this.
|
||||
func CreateLicenseKeyCustom(ctx context.Context, key *LicenseKey, rawKey string) error {
|
||||
key.KeyHash = HashKey(rawKey)
|
||||
if len(rawKey) > 12 {
|
||||
key.KeyPrefix = rawKey[:12] + "..."
|
||||
} else {
|
||||
key.KeyPrefix = rawKey
|
||||
}
|
||||
_, err := db.GetEngine(ctx).Insert(key)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetLicenseKeyByHash looks up a key by its SHA-256 hash.
|
||||
func GetLicenseKeyByHash(ctx context.Context, hash string) (*LicenseKey, error) {
|
||||
key := new(LicenseKey)
|
||||
@@ -160,6 +173,8 @@ func DeleteLicenseKey(ctx context.Context, id int64) error {
|
||||
// Returns the key record and its associated package, or an error.
|
||||
// The domain parameter is optional — when provided, it is checked against
|
||||
// the key's DomainRestriction list and the MaxSites limit.
|
||||
// On first heartbeat with a domain, if no DomainRestriction is set, the domain
|
||||
// is automatically associated as the key's restriction (lock-on-first-use).
|
||||
func ValidateLicenseKey(ctx context.Context, rawKey, domain string) (*LicenseKey, *LicensePackage, error) {
|
||||
hash := HashKey(rawKey)
|
||||
key, err := GetLicenseKeyByHash(ctx, hash)
|
||||
@@ -201,6 +216,32 @@ func ValidateLicenseKey(ctx context.Context, rawKey, domain string) (*LicenseKey
|
||||
if !allowed {
|
||||
return nil, nil, fmt.Errorf("domain not allowed for this license key")
|
||||
}
|
||||
} else {
|
||||
// No domain restriction set — auto-associate on first heartbeat.
|
||||
// Append this domain to the restriction list, enforcing max_sites.
|
||||
maxSites := key.MaxSites
|
||||
if maxSites == 0 {
|
||||
maxSites = pkg.MaxSites
|
||||
}
|
||||
domainKnown, _ := IsDomainKnownForKey(ctx, key.ID, domain)
|
||||
if !domainKnown {
|
||||
if maxSites > 0 {
|
||||
uniqueDomains, err := CountUniqueDomainsByKey(ctx, key.ID)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to count domains: %w", err)
|
||||
}
|
||||
if uniqueDomains >= int64(maxSites) {
|
||||
return nil, nil, fmt.Errorf("site limit reached (%d/%d)", uniqueDomains, maxSites)
|
||||
}
|
||||
}
|
||||
// Append this domain to the key's restriction list.
|
||||
_ = updateDomainRestriction(ctx, key.ID, domain)
|
||||
if key.DomainRestriction == "" {
|
||||
key.DomainRestriction = domain
|
||||
} else {
|
||||
key.DomainRestriction = key.DomainRestriction + "," + domain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Site limit check: use key's MaxSites, fall back to package default.
|
||||
@@ -223,3 +264,42 @@ func ValidateLicenseKey(ctx context.Context, rawKey, domain string) (*LicenseKey
|
||||
|
||||
return key, pkg, nil
|
||||
}
|
||||
|
||||
// updateDomainRestriction appends a domain to a key's DomainRestriction field in the DB.
|
||||
func updateDomainRestriction(ctx context.Context, keyID int64, domain string) error {
|
||||
key, err := GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if key.DomainRestriction == "" {
|
||||
key.DomainRestriction = domain
|
||||
} else {
|
||||
key.DomainRestriction = key.DomainRestriction + "," + domain
|
||||
}
|
||||
_, err = db.GetEngine(ctx).ID(keyID).Cols("domain_restriction").Update(key)
|
||||
return err
|
||||
}
|
||||
|
||||
// RenewLicenseKey extends the expiration of a key by the given number of days
|
||||
// from the current expiry (or from now if already expired/no expiry set).
|
||||
func RenewLicenseKey(ctx context.Context, keyID int64, days int) error {
|
||||
key, err := GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := timeutil.TimeStampNow()
|
||||
var base timeutil.TimeStamp
|
||||
if key.ExpiresUnix > 0 && key.ExpiresUnix > now {
|
||||
// Key still valid — extend from current expiry.
|
||||
base = key.ExpiresUnix
|
||||
} else {
|
||||
// Key expired or has no expiry — extend from now.
|
||||
base = now
|
||||
}
|
||||
|
||||
key.ExpiresUnix = base + timeutil.TimeStamp(int64(days)*86400)
|
||||
key.IsActive = true
|
||||
_, err = db.GetEngine(ctx).ID(keyID).Cols("expires_unix", "is_active").Update(key)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -83,6 +83,11 @@ func UpdateLicensePackage(ctx context.Context, pkg *LicensePackage) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// CountOrgPackages returns the number of license packages for an organization.
|
||||
func CountOrgPackages(ctx context.Context, orgID int64) (int64, error) {
|
||||
return db.GetEngine(ctx).Where("owner_id = ?", orgID).Count(new(LicensePackage))
|
||||
}
|
||||
|
||||
// DeleteLicensePackage deletes a license package by ID.
|
||||
func DeleteLicensePackage(ctx context.Context, id int64) error {
|
||||
_, err := db.GetEngine(ctx).ID(id).Delete(new(LicensePackage))
|
||||
|
||||
+12
-3
@@ -33,9 +33,7 @@ const (
|
||||
TypeProjects // 8 Projects
|
||||
TypePackages // 9 Packages
|
||||
TypeActions // 10 Actions
|
||||
|
||||
// FIXME: TEAM-UNIT-PERMISSION: the team unit "admin" permission's design is not right, when a new unit is added in the future,
|
||||
// admin team won't inherit the correct admin permission for the new unit, need to have a complete fix before adding any new unit.
|
||||
TypeLicenses // 11 Licenses
|
||||
)
|
||||
|
||||
// Value returns integer value for unit type (used by template)
|
||||
@@ -65,6 +63,7 @@ var (
|
||||
TypeProjects,
|
||||
TypePackages,
|
||||
TypeActions,
|
||||
TypeLicenses,
|
||||
}
|
||||
|
||||
// DefaultRepoUnits contains the default unit types
|
||||
@@ -328,6 +327,15 @@ var (
|
||||
perm.AccessModeOwner,
|
||||
}
|
||||
|
||||
UnitLicenses = Unit{
|
||||
TypeLicenses,
|
||||
"repo.licenses",
|
||||
"/licenses",
|
||||
"repo.licenses.desc",
|
||||
8,
|
||||
perm.AccessModeOwner,
|
||||
}
|
||||
|
||||
// Units contains all the units
|
||||
Units = map[Type]Unit{
|
||||
TypeCode: UnitCode,
|
||||
@@ -340,6 +348,7 @@ var (
|
||||
TypeProjects: UnitProjects,
|
||||
TypePackages: UnitPackages,
|
||||
TypeActions: UnitActions,
|
||||
TypeLicenses: UnitLicenses,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -2635,7 +2635,7 @@
|
||||
"repo.licenses.active": "Active",
|
||||
"repo.licenses.inactive": "Inactive",
|
||||
"repo.licenses.none": "No License Packages",
|
||||
"repo.licenses.none_desc": "License packages can be created via the API to gate access to update streams.",
|
||||
"repo.licenses.none_desc": "Create a license package to start managing keys and gating update feeds.",
|
||||
"repo.licenses.issued_keys": "Issued Keys",
|
||||
"repo.licenses.key_prefix": "Key",
|
||||
"repo.licenses.licensee": "Licensee",
|
||||
@@ -2650,7 +2650,7 @@
|
||||
"repo.licenses.package_created": "License package created successfully.",
|
||||
"repo.licenses.generate_key": "Generate Key",
|
||||
"repo.licenses.key_created": "License Key Created",
|
||||
"repo.licenses.key_created_copy": "Copy this key now. It will not be shown again.",
|
||||
"repo.licenses.key_created_copy": "This key is hashed before storage and cannot be retrieved later. Copy and store it securely now.",
|
||||
"repo.licenses.revoke": "Revoke",
|
||||
"repo.licenses.edit_package": "Edit License Package",
|
||||
"repo.licenses.delete_package": "Delete Package",
|
||||
@@ -2670,6 +2670,22 @@
|
||||
"repo.licenses.expires_at_help": "Leave empty for no expiry (lifetime).",
|
||||
"repo.licenses.key_updated": "License key updated.",
|
||||
"repo.licenses.last_seen": "Last Seen",
|
||||
"repo.licenses.confirm_delete_package": "Delete this package? This action cannot be undone.",
|
||||
"repo.licenses.confirm_revoke_key": "Revoke this license key? The licensee will immediately lose access to update feeds.",
|
||||
"repo.licenses.feed_joomla_xml": "Joomla XML",
|
||||
"repo.licenses.feed_dolibarr_json": "Dolibarr JSON",
|
||||
"repo.licenses.feed_joomla_updates": "Joomla updates.xml",
|
||||
"repo.licenses.feed_dolibarr_updates": "Dolibarr JSON",
|
||||
"repo.licenses.master_label": "Master",
|
||||
"repo.licenses.unlimited": "unlimited",
|
||||
"repo.licenses.active_help_package": "Deactivating blocks new key creation and disables all issued keys.",
|
||||
"repo.licenses.active_help_key": "Deactivating immediately blocks update feed access for this licensee.",
|
||||
"repo.licenses.renew": "Renew",
|
||||
"repo.licenses.key_renewed": "License key renewed for %d days.",
|
||||
"repo.licenses.confirm_renew_key": "Renew this license key? The expiration will be extended by the package duration.",
|
||||
"repo.licenses.desc": "License packages and keys for gating update feeds.",
|
||||
"repo.licenses.custom_key_placeholder": "Custom key (optional)",
|
||||
"repo.licenses.custom_key_help": "Leave empty to auto-generate. Site admins and org owners can set a custom key value.",
|
||||
"repo.release.draft": "Draft",
|
||||
"repo.release.prerelease": "Pre-Release",
|
||||
"repo.release.stable": "Stable",
|
||||
|
||||
@@ -111,6 +111,10 @@ func home(ctx *context.Context, viewRepositories bool) {
|
||||
|
||||
orgCfg, _ := licenses_model.GetOrgConfig(ctx, ctx.Org.Organization.ID)
|
||||
ctx.Data["OrgLicensingEnabled"] = orgCfg != nil && orgCfg.LicensingEnabled
|
||||
if orgCfg != nil && orgCfg.LicensingEnabled {
|
||||
numPkgs, _ := licenses_model.CountOrgPackages(ctx, ctx.Org.Organization.ID)
|
||||
ctx.Data["NumOrgLicensePackages"] = numPkgs
|
||||
}
|
||||
ctx.Data["IsPublicMember"] = func(uid int64) bool {
|
||||
return membersIsPublic[uid]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"time"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm"
|
||||
unit_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
@@ -54,8 +56,10 @@ func Licenses(ctx *context.Context) {
|
||||
org := ctx.Org.Organization
|
||||
ownerID := org.ID
|
||||
|
||||
// Auto-create master key if org owner.
|
||||
if ctx.Org.IsOwner {
|
||||
canWriteLicenses := ctx.Org.Organization.UnitPermission(ctx, ctx.Doer, unit_model.TypeLicenses) >= perm.AccessModeWrite || ctx.IsUserSiteAdmin()
|
||||
|
||||
// Auto-create master key if has write access.
|
||||
if canWriteLicenses {
|
||||
newMasterKey, err := licenses.EnsureMasterKey(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("EnsureMasterKey", err)
|
||||
@@ -89,7 +93,7 @@ func Licenses(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
ctx.Data["LicenseKeys"] = keys
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Org.IsOwner
|
||||
ctx.Data["IsRepoAdmin"] = canWriteLicenses
|
||||
ctx.Data["IsSiteAdmin"] = ctx.IsUserSiteAdmin()
|
||||
ctx.Data["OrgLicensingEnabled"] = true
|
||||
|
||||
@@ -168,10 +172,21 @@ func LicensesGenerateKey(ctx *context.Context) {
|
||||
key.ExpiresUnix = timeutil.TimeStamp(expires.Unix())
|
||||
}
|
||||
|
||||
rawKey, err := licenses.CreateLicenseKey(ctx, key)
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateLicenseKey", err)
|
||||
return
|
||||
// Site admins and org owners can manually set a custom key.
|
||||
var rawKey string
|
||||
customKey := strings.TrimSpace(ctx.FormString("custom_key"))
|
||||
if customKey != "" && (ctx.IsUserSiteAdmin() || ctx.Org.IsOwner) {
|
||||
if err := licenses.CreateLicenseKeyCustom(ctx, key, customKey); err != nil {
|
||||
ctx.ServerError("CreateLicenseKeyCustom", err)
|
||||
return
|
||||
}
|
||||
rawKey = customKey
|
||||
} else {
|
||||
rawKey, err = licenses.CreateLicenseKey(ctx, key)
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateLicenseKey", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Re-render with the new key shown.
|
||||
@@ -392,3 +407,32 @@ func LicensesRevokeKey(ctx *context.Context) {
|
||||
ctx.Flash.Success(ctx.Tr("repo.licenses.key_revoked"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
|
||||
}
|
||||
|
||||
// LicensesRenewKey extends a license key's expiration by the package's duration.
|
||||
func LicensesRenewKey(ctx *context.Context) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicenseKeyByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, key.PackageID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
days := pkg.DurationDays
|
||||
if days == 0 {
|
||||
days = 365 // default to 1 year for lifetime packages
|
||||
}
|
||||
|
||||
if err := licenses.RenewLicenseKey(ctx, keyID, days); err != nil {
|
||||
ctx.ServerError("RenewLicenseKey", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.licenses.key_renewed", days))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
unit_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
@@ -51,13 +52,14 @@ func Licenses(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.licenses")
|
||||
ctx.Data["PageIsLicenses"] = true
|
||||
ctx.Data["IsLicensesPage"] = true
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Repo.Permission.IsAdmin()
|
||||
canWriteLicenses := ctx.Repo.Permission.CanWrite(unit_model.TypeLicenses) || ctx.IsUserSiteAdmin()
|
||||
ctx.Data["IsRepoAdmin"] = canWriteLicenses
|
||||
ctx.Data["IsSiteAdmin"] = ctx.IsUserSiteAdmin()
|
||||
|
||||
ownerID := ctx.Repo.Repository.OwnerID
|
||||
|
||||
// Auto-create master package + key if admin and none exist.
|
||||
if ctx.Repo.Permission.IsAdmin() {
|
||||
if canWriteLicenses {
|
||||
newMasterKey, err := licenses.EnsureMasterKey(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("EnsureMasterKey", err)
|
||||
@@ -169,16 +171,27 @@ func LicensesGenerateKey(ctx *context.Context) {
|
||||
key.ExpiresUnix = timeutil.TimeStamp(expires.Unix())
|
||||
}
|
||||
|
||||
rawKey, err := licenses.CreateLicenseKey(ctx, key)
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateLicenseKey", err)
|
||||
return
|
||||
// Site admins and org owners can manually set a custom key.
|
||||
var rawKey string
|
||||
customKey := strings.TrimSpace(ctx.FormString("custom_key"))
|
||||
if customKey != "" && (ctx.IsUserSiteAdmin() || ctx.Repo.Permission.IsOwner()) {
|
||||
if err := licenses.CreateLicenseKeyCustom(ctx, key, customKey); err != nil {
|
||||
ctx.ServerError("CreateLicenseKeyCustom", err)
|
||||
return
|
||||
}
|
||||
rawKey = customKey
|
||||
} else {
|
||||
rawKey, err = licenses.CreateLicenseKey(ctx, key)
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateLicenseKey", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("repo.licenses")
|
||||
ctx.Data["PageIsLicenses"] = true
|
||||
ctx.Data["IsLicensesPage"] = true
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Repo.Permission.IsAdmin()
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Repo.Permission.CanWrite(unit_model.TypeLicenses) || ctx.IsUserSiteAdmin()
|
||||
ctx.Data["IsSiteAdmin"] = ctx.IsUserSiteAdmin()
|
||||
ctx.Data["NewKeyCreated"] = rawKey
|
||||
|
||||
@@ -398,3 +411,32 @@ func LicensesDeletePackage(ctx *context.Context) {
|
||||
ctx.Flash.Success(ctx.Tr("repo.licenses.package_deleted"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
|
||||
}
|
||||
|
||||
// LicensesRenewKey extends a license key's expiration by the package's duration.
|
||||
func LicensesRenewKey(ctx *context.Context) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicenseKeyByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, key.PackageID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
days := pkg.DurationDays
|
||||
if days == 0 {
|
||||
days = 365 // default to 1 year for lifetime packages
|
||||
}
|
||||
|
||||
if err := licenses.RenewLicenseKey(ctx, keyID, days); err != nil {
|
||||
ctx.ServerError("RenewLicenseKey", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.licenses.key_renewed", days))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
|
||||
}
|
||||
|
||||
+24
-18
@@ -1106,15 +1106,18 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
|
||||
m.Group("/licenses", func() {
|
||||
m.Get("", org.Licenses)
|
||||
m.Post("/packages", org.LicensesCreatePackage)
|
||||
m.Get("/packages/{id}/edit", org.LicensesEditPackage)
|
||||
m.Post("/packages/{id}/edit", org.LicensesEditPackagePost)
|
||||
m.Post("/packages/{id}/delete", org.LicensesDeletePackage)
|
||||
m.Post("/keys/generate", org.LicensesGenerateKey)
|
||||
m.Get("/keys/{id}/edit", org.LicensesEditKey)
|
||||
m.Post("/keys/{id}/edit", org.LicensesEditKeyPost)
|
||||
m.Post("/keys/{id}/revoke", org.LicensesRevokeKey)
|
||||
})
|
||||
m.Group("", func() {
|
||||
m.Post("/packages", org.LicensesCreatePackage)
|
||||
m.Get("/packages/{id}/edit", org.LicensesEditPackage)
|
||||
m.Post("/packages/{id}/edit", org.LicensesEditPackagePost)
|
||||
m.Post("/packages/{id}/delete", org.LicensesDeletePackage)
|
||||
m.Post("/keys/generate", org.LicensesGenerateKey)
|
||||
m.Get("/keys/{id}/edit", org.LicensesEditKey)
|
||||
m.Post("/keys/{id}/edit", org.LicensesEditKeyPost)
|
||||
m.Post("/keys/{id}/revoke", org.LicensesRevokeKey)
|
||||
m.Post("/keys/{id}/renew", org.LicensesRenewKey)
|
||||
}, reqUnitAccess(unit.TypeLicenses, perm.AccessModeWrite, true))
|
||||
}, reqUnitAccess(unit.TypeLicenses, perm.AccessModeRead, true))
|
||||
|
||||
m.Get("/repositories", org.Repositories)
|
||||
m.Get("/heatmap", user.DashboardHeatmap)
|
||||
@@ -1521,15 +1524,18 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
// "/{username}/{reponame}": licenses page
|
||||
m.Group("/{username}/{reponame}/licenses", func() {
|
||||
m.Get("", repo.Licenses)
|
||||
m.Post("/packages", repo.LicensesCreatePackage)
|
||||
m.Get("/packages/{id}/edit", repo.LicensesEditPackage)
|
||||
m.Post("/packages/{id}/edit", repo.LicensesEditPackagePost)
|
||||
m.Post("/packages/{id}/delete", repo.LicensesDeletePackage)
|
||||
m.Post("/keys/generate", repo.LicensesGenerateKey)
|
||||
m.Get("/keys/{id}/edit", repo.LicensesEditKey)
|
||||
m.Post("/keys/{id}/edit", repo.LicensesEditKeyPost)
|
||||
m.Post("/keys/{id}/revoke", repo.LicensesRevokeKey)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
m.Group("", func() {
|
||||
m.Post("/packages", repo.LicensesCreatePackage)
|
||||
m.Get("/packages/{id}/edit", repo.LicensesEditPackage)
|
||||
m.Post("/packages/{id}/edit", repo.LicensesEditPackagePost)
|
||||
m.Post("/packages/{id}/delete", repo.LicensesDeletePackage)
|
||||
m.Post("/keys/generate", repo.LicensesGenerateKey)
|
||||
m.Get("/keys/{id}/edit", repo.LicensesEditKey)
|
||||
m.Post("/keys/{id}/edit", repo.LicensesEditKeyPost)
|
||||
m.Post("/keys/{id}/revoke", repo.LicensesRevokeKey)
|
||||
m.Post("/keys/{id}/renew", repo.LicensesRenewKey)
|
||||
}, context.RequireUnitWriter(unit.TypeLicenses))
|
||||
}, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeLicenses))
|
||||
// end "/{username}/{reponame}": licenses
|
||||
|
||||
m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments
|
||||
|
||||
+26
-20
@@ -7,7 +7,10 @@
|
||||
<div class="ui info message">
|
||||
<div class="header">{{ctx.Locale.Tr "repo.licenses.master_key_created"}}</div>
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.master_key_created_copy"}}</p>
|
||||
<code class="tw-text-lg tw-select-all">{{.NewMasterKey}}</code>
|
||||
<div class="ui action input tw-w-full tw-mt-2">
|
||||
<input class="js-new-master-key" type="text" readonly value="{{.NewMasterKey}}" onclick="this.select()">
|
||||
<button class="ui button" data-clipboard-target=".js-new-master-key" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 14}}</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -15,7 +18,10 @@
|
||||
<div class="ui success message">
|
||||
<div class="header">{{ctx.Locale.Tr "repo.licenses.key_created"}}</div>
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.key_created_copy"}}</p>
|
||||
<code class="tw-text-lg tw-select-all">{{.NewKeyCreated}}</code>
|
||||
<div class="ui action input tw-w-full tw-mt-2">
|
||||
<input class="js-new-license-key" type="text" readonly value="{{.NewKeyCreated}}" onclick="this.select()">
|
||||
<button class="ui button" data-clipboard-target=".js-new-license-key" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 14}}</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -48,7 +54,7 @@
|
||||
<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 = unlimited</p>
|
||||
<p class="help">0 = {{ctx.Locale.Tr "repo.licenses.unlimited"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.channels"}}</label>
|
||||
@@ -69,7 +75,7 @@
|
||||
</details>
|
||||
{{end}}
|
||||
{{if .LicensePackages}}
|
||||
<table class="ui table">
|
||||
<table class="ui compact table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.package_name"}}</th>
|
||||
@@ -83,16 +89,19 @@
|
||||
<tbody>
|
||||
{{range .LicensePackages}}
|
||||
<tr>
|
||||
<td><strong>{{.Name}}</strong>{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}</td>
|
||||
<td><strong>{{.Name}}</strong>{{if eq .Name "Master (Internal)"}} <span class="ui tiny orange label">{{ctx.Locale.Tr "repo.licenses.master_label"}}</span>{{end}}{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}</td>
|
||||
<td>{{if eq .DurationDays 0}}{{ctx.Locale.Tr "repo.licenses.lifetime"}}{{else}}{{.DurationDays}} {{ctx.Locale.Tr "repo.licenses.days"}}{{end}}</td>
|
||||
<td>{{if .AllowedChannels}}<code>{{.AllowedChannels}}</code>{{else}}{{ctx.Locale.Tr "repo.licenses.all_channels"}}{{end}}</td>
|
||||
<td>{{.KeyCount}}</td>
|
||||
<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="{{$.Org.HomeLink}}/-/licenses/keys/generate" class="tw-inline">
|
||||
<form method="post" action="{{$.Org.HomeLink}}/-/licenses/keys/generate" class="tw-inline tw-flex tw-gap-1 tw-items-center">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="package_id" value="{{.ID}}">
|
||||
{{if or $.IsSiteAdmin $.IsOrganizationOwner}}
|
||||
<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>
|
||||
@@ -102,12 +111,9 @@
|
||||
{{svg "octicon-pencil" 14}}
|
||||
</a>
|
||||
{{if $.IsSiteAdmin}}
|
||||
<form method="post" action="{{$.Org.HomeLink}}/-/licenses/packages/{{.ID}}/delete" class="tw-inline" onsubmit="return confirm('Delete this package? This action cannot be undone.')">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<button class="ui tiny red button" type="submit" title="{{ctx.Locale.Tr "repo.licenses.delete_package"}}">
|
||||
{{svg "octicon-trash" 14}}
|
||||
</button>
|
||||
</form>
|
||||
<button class="ui tiny red button link-action" data-url="{{$.Org.HomeLink}}/-/licenses/packages/{{.ID}}/delete" data-modal-confirm="{{ctx.Locale.Tr "repo.licenses.confirm_delete_package"}}" title="{{ctx.Locale.Tr "repo.licenses.delete_package"}}">
|
||||
{{svg "octicon-trash" 14}}
|
||||
</button>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</td>
|
||||
@@ -130,7 +136,7 @@
|
||||
{{svg "octicon-lock" 16}} {{ctx.Locale.Tr "repo.licenses.issued_keys"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<table class="ui table">
|
||||
<table class="ui compact table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.key_prefix"}}</th>
|
||||
@@ -144,7 +150,7 @@
|
||||
<tbody>
|
||||
{{range .LicenseKeys}}
|
||||
<tr>
|
||||
<td><code>{{.KeyPrefix}}</code>{{if .IsInternal}} <span class="ui tiny orange label">Master</span>{{end}}</td>
|
||||
<td><code>{{.KeyPrefix}}</code>{{if .IsInternal}} <span class="ui tiny orange label">{{ctx.Locale.Tr "repo.licenses.master_label"}}</span>{{end}}</td>
|
||||
<td>{{.LicenseeName}}{{if .LicenseeEmail}} <small>({{.LicenseeEmail}})</small>{{end}}</td>
|
||||
<td>{{if eq .ExpiresUnix 0}}{{ctx.Locale.Tr "repo.licenses.never"}}{{else}}{{DateUtils.AbsoluteShort .ExpiresUnix}}{{end}}</td>
|
||||
<td>{{if eq .LastHeartbeatUnix 0}}{{ctx.Locale.Tr "repo.licenses.never"}}{{else}}{{DateUtils.AbsoluteShort .LastHeartbeatUnix}}{{end}}</td>
|
||||
@@ -155,13 +161,13 @@
|
||||
<a class="ui tiny button" href="{{$.Org.HomeLink}}/-/licenses/keys/{{.ID}}/edit" title="{{ctx.Locale.Tr "repo.licenses.edit_key"}}">
|
||||
{{svg "octicon-pencil" 14}}
|
||||
</a>
|
||||
<button class="ui tiny green button link-action" data-url="{{$.Org.HomeLink}}/-/licenses/keys/{{.ID}}/renew" data-modal-confirm="{{ctx.Locale.Tr "repo.licenses.confirm_renew_key"}}" title="{{ctx.Locale.Tr "repo.licenses.renew"}}">
|
||||
{{svg "octicon-sync" 14}}
|
||||
</button>
|
||||
{{end}}
|
||||
<form method="post" action="{{$.Org.HomeLink}}/-/licenses/keys/{{.ID}}/revoke" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<button class="ui tiny red button" type="submit" title="{{ctx.Locale.Tr "repo.licenses.revoke"}}">
|
||||
{{svg "octicon-x" 14}}
|
||||
</button>
|
||||
</form>
|
||||
<button class="ui tiny red button link-action" data-url="{{$.Org.HomeLink}}/-/licenses/keys/{{.ID}}/revoke" data-modal-confirm="{{ctx.Locale.Tr "repo.licenses.confirm_revoke_key"}}" title="{{ctx.Locale.Tr "repo.licenses.revoke"}}">
|
||||
{{svg "octicon-x" 14}}
|
||||
</button>
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.max_sites"}}</label>
|
||||
<input name="max_sites" type="number" value="{{.Package.MaxSites}}" min="0">
|
||||
<p class="help">0 = unlimited</p>
|
||||
<p class="help">0 = {{ctx.Locale.Tr "repo.licenses.unlimited"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.channels"}}</label>
|
||||
@@ -47,6 +47,7 @@
|
||||
<input name="is_active" type="checkbox" {{if .Package.IsActive}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.active"}}</label>
|
||||
</div>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.licenses.active_help_package"}}</p>
|
||||
</div>
|
||||
<div class="field tw-mt-4">
|
||||
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "save"}}</button>
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
{{if and .IsOrganizationMember (or .OrgLicensingEnabled .IsLicensesPage)}}
|
||||
<a class="{{if .IsLicensesPage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/licenses">
|
||||
{{svg "octicon-key"}} {{ctx.Locale.Tr "repo.licenses"}}
|
||||
{{if .NumOrgLicensePackages}}
|
||||
<div class="ui small label">{{.NumOrgLicensePackages}}</div>
|
||||
{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if and .IsRepoIndexerEnabled .CanReadCode}}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsSettingsUpdateStreams}}active {{end}}item" href="{{.OrgLink}}/settings/update-streams">
|
||||
{{ctx.Locale.Tr "org.settings.update_streams"}}
|
||||
{{svg "octicon-key"}} {{ctx.Locale.Tr "org.settings.update_streams"}}
|
||||
</a>
|
||||
{{if .EnableActions}}
|
||||
<details class="item toggleable-item" {{if or .PageIsOrgSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
<div class="ui info message">
|
||||
<div class="header">{{ctx.Locale.Tr "repo.licenses.master_key_created"}}</div>
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.master_key_created_copy"}}</p>
|
||||
<code class="tw-text-lg tw-select-all">{{.NewMasterKey}}</code>
|
||||
<div class="ui action input tw-w-full tw-mt-2">
|
||||
<input class="js-new-master-key" type="text" readonly value="{{.NewMasterKey}}" onclick="this.select()">
|
||||
<button class="ui button" data-clipboard-target=".js-new-master-key" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 14}}</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -15,7 +18,10 @@
|
||||
<div class="ui success message">
|
||||
<div class="header">{{ctx.Locale.Tr "repo.licenses.key_created"}}</div>
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.key_created_copy"}}</p>
|
||||
<code class="tw-text-lg tw-select-all">{{.NewKeyCreated}}</code>
|
||||
<div class="ui action input tw-w-full tw-mt-2">
|
||||
<input class="js-new-license-key" type="text" readonly value="{{.NewKeyCreated}}" onclick="this.select()">
|
||||
<button class="ui button" data-clipboard-target=".js-new-license-key" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 14}}</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -25,7 +31,7 @@
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
{{if .LicensePackages}}
|
||||
<table class="ui table">
|
||||
<table class="ui compact table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.package_name"}}</th>
|
||||
@@ -39,16 +45,19 @@
|
||||
<tbody>
|
||||
{{range .LicensePackages}}
|
||||
<tr>
|
||||
<td><strong>{{.Name}}</strong>{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}</td>
|
||||
<td><strong>{{.Name}}</strong>{{if eq .Name "Master (Internal)"}} <span class="ui tiny orange label">{{ctx.Locale.Tr "repo.licenses.master_label"}}</span>{{end}}{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}</td>
|
||||
<td>{{if eq .DurationDays 0}}{{ctx.Locale.Tr "repo.licenses.lifetime"}}{{else}}{{.DurationDays}} {{ctx.Locale.Tr "repo.licenses.days"}}{{end}}</td>
|
||||
<td>{{if .AllowedChannels}}<code>{{.AllowedChannels}}</code>{{else}}{{ctx.Locale.Tr "repo.licenses.all_channels"}}{{end}}</td>
|
||||
<td>{{.KeyCount}}</td>
|
||||
<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">
|
||||
<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>
|
||||
@@ -58,12 +67,9 @@
|
||||
{{svg "octicon-pencil" 14}}
|
||||
</a>
|
||||
{{if $.IsSiteAdmin}}
|
||||
<form method="post" action="{{$.RepoLink}}/licenses/packages/{{.ID}}/delete" class="tw-inline" onsubmit="return confirm('Delete this package? This action cannot be undone.')">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<button class="ui tiny red button" type="submit" title="{{ctx.Locale.Tr "repo.licenses.delete_package"}}">
|
||||
{{svg "octicon-trash" 14}}
|
||||
</button>
|
||||
</form>
|
||||
<button class="ui tiny red button link-action" data-url="{{$.RepoLink}}/licenses/packages/{{.ID}}/delete" data-modal-confirm="{{ctx.Locale.Tr "repo.licenses.confirm_delete_package"}}" title="{{ctx.Locale.Tr "repo.licenses.delete_package"}}">
|
||||
{{svg "octicon-trash" 14}}
|
||||
</button>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</td>
|
||||
@@ -108,7 +114,7 @@
|
||||
<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 = unlimited</p>
|
||||
<p class="help">0 = {{ctx.Locale.Tr "repo.licenses.unlimited"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.channels"}}</label>
|
||||
@@ -136,7 +142,7 @@
|
||||
{{svg "octicon-lock" 16}} {{ctx.Locale.Tr "repo.licenses.issued_keys"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<table class="ui table">
|
||||
<table class="ui compact table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.key_prefix"}}</th>
|
||||
@@ -150,7 +156,7 @@
|
||||
<tbody>
|
||||
{{range .LicenseKeys}}
|
||||
<tr>
|
||||
<td><code>{{.KeyPrefix}}</code>{{if .IsInternal}} <span class="ui tiny orange label">Master</span>{{end}}</td>
|
||||
<td><code>{{.KeyPrefix}}</code>{{if .IsInternal}} <span class="ui tiny orange label">{{ctx.Locale.Tr "repo.licenses.master_label"}}</span>{{end}}</td>
|
||||
<td>{{.LicenseeName}}{{if .LicenseeEmail}} <small>({{.LicenseeEmail}})</small>{{end}}</td>
|
||||
<td>{{if eq .ExpiresUnix 0}}{{ctx.Locale.Tr "repo.licenses.never"}}{{else}}{{DateUtils.AbsoluteShort .ExpiresUnix}}{{end}}</td>
|
||||
<td>{{if eq .LastHeartbeatUnix 0}}{{ctx.Locale.Tr "repo.licenses.never"}}{{else}}{{DateUtils.AbsoluteShort .LastHeartbeatUnix}}{{end}}</td>
|
||||
@@ -161,13 +167,13 @@
|
||||
<a class="ui tiny button" href="{{$.RepoLink}}/licenses/keys/{{.ID}}/edit" title="{{ctx.Locale.Tr "repo.licenses.edit_key"}}">
|
||||
{{svg "octicon-pencil" 14}}
|
||||
</a>
|
||||
<button class="ui tiny green button link-action" data-url="{{$.RepoLink}}/licenses/keys/{{.ID}}/renew" data-modal-confirm="{{ctx.Locale.Tr "repo.licenses.confirm_renew_key"}}" title="{{ctx.Locale.Tr "repo.licenses.renew"}}">
|
||||
{{svg "octicon-sync" 14}}
|
||||
</button>
|
||||
{{end}}
|
||||
<form method="post" action="{{$.RepoLink}}/licenses/keys/{{.ID}}/revoke" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<button class="ui tiny red button" type="submit" title="{{ctx.Locale.Tr "repo.licenses.revoke"}}">
|
||||
{{svg "octicon-x" 14}}
|
||||
</button>
|
||||
</form>
|
||||
<button class="ui tiny red button link-action" data-url="{{$.RepoLink}}/licenses/keys/{{.ID}}/revoke" data-modal-confirm="{{ctx.Locale.Tr "repo.licenses.confirm_revoke_key"}}" title="{{ctx.Locale.Tr "repo.licenses.revoke"}}">
|
||||
{{svg "octicon-x" 14}}
|
||||
</button>
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
@@ -183,17 +189,17 @@
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<div class="field">
|
||||
<label>Joomla updates.xml</label>
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.feed_joomla_updates"}}</label>
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="text" readonly value="{{.Repository.HTMLURL ctx}}/updates.xml" onclick="this.select()">
|
||||
<button class="ui button" onclick="navigator.clipboard.writeText(this.previousElementSibling.value)">{{svg "octicon-copy" 14}}</button>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field tw-mt-2">
|
||||
<label>Dolibarr JSON</label>
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.feed_dolibarr_updates"}}</label>
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="text" readonly value="{{.Repository.HTMLURL ctx}}/updates/dolibarr.json" onclick="this.select()">
|
||||
<button class="ui button" onclick="navigator.clipboard.writeText(this.previousElementSibling.value)">{{svg "octicon-copy" 14}}</button>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
<input name="is_active" type="checkbox" {{if .Key.IsActive}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.active"}}</label>
|
||||
</div>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.licenses.active_help_key"}}</p>
|
||||
</div>
|
||||
<div class="field tw-mt-4">
|
||||
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "save"}}</button>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.max_sites"}}</label>
|
||||
<input name="max_sites" type="number" value="{{.Package.MaxSites}}" min="0">
|
||||
<p class="help">0 = unlimited</p>
|
||||
<p class="help">0 = {{ctx.Locale.Tr "repo.licenses.unlimited"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.channels"}}</label>
|
||||
@@ -47,6 +47,7 @@
|
||||
<input name="is_active" type="checkbox" {{if .Package.IsActive}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.active"}}</label>
|
||||
</div>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.licenses.active_help_package"}}</p>
|
||||
</div>
|
||||
<div class="field tw-mt-4">
|
||||
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "save"}}</button>
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
{{svg "octicon-rss" 16}} {{ctx.Locale.Tr "rss_feed"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if not .PageIsTagList}}
|
||||
{{if and (not .PageIsTagList) .LicensingEnabled}}
|
||||
{{if or (eq .RepoUpdatePlatform "joomla") (eq .RepoUpdatePlatform "both") (eq .RepoUpdatePlatform "")}}
|
||||
<a class="ui small button" href="{{.RepoLink}}/updates.xml" target="_blank">
|
||||
{{svg "octicon-download" 16}} Joomla XML
|
||||
{{svg "octicon-download" 16}} {{ctx.Locale.Tr "repo.licenses.feed_joomla_xml"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if or (eq .RepoUpdatePlatform "dolibarr") (eq .RepoUpdatePlatform "both")}}
|
||||
<a class="ui small button" href="{{.RepoLink}}/updates/dolibarr.json" target="_blank">
|
||||
{{svg "octicon-download" 16}} Dolibarr JSON
|
||||
{{svg "octicon-download" 16}} {{ctx.Locale.Tr "repo.licenses.feed_dolibarr_json"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user