From 68ee152cfcc07f65f07afab993645369f1773b9e Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 09:54:38 -0500 Subject: [PATCH 1/2] fix(licenses): show feed URLs based on repo update platform setting Only show Joomla XML URL when platform is joomla/both/empty. Only show Dolibarr JSON URL when platform is dolibarr/both. Also gate the entire feed URLs section behind LicensingEnabled. Co-Authored-By: Claude Opus 4.6 (1M context) --- templates/repo/licenses.tmpl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/templates/repo/licenses.tmpl b/templates/repo/licenses.tmpl index e03d5dad11..8364d208e6 100644 --- a/templates/repo/licenses.tmpl +++ b/templates/repo/licenses.tmpl @@ -184,10 +184,12 @@ {{end}} {{/* ── Update Feed URLs ── */}} + {{if .LicensingEnabled}}

{{svg "octicon-rss" 16}} {{ctx.Locale.Tr "repo.licenses.update_feeds"}}

+ {{if or (eq .RepoUpdatePlatform "joomla") (eq .RepoUpdatePlatform "both") (eq .RepoUpdatePlatform "")}}
@@ -195,6 +197,8 @@
+ {{end}} + {{if or (eq .RepoUpdatePlatform "dolibarr") (eq .RepoUpdatePlatform "both")}}
@@ -202,7 +206,9 @@
+ {{end}}
+ {{end}} {{template "base/footer" .}} From 4efc679c8b975919f03038a3ce3167a8fd07fc95 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 31 May 2026 10:03:12 -0500 Subject: [PATCH 2/2] feat(licenses): platform enforcement, key deletion, expired key cleanup - Block update feed endpoints based on repo platform setting: Joomla-only repos return 404 on /updates/dolibarr.json and vice versa - Show feed URLs section only when licensing is enabled - Add delete button for license keys (site admin only) - Add weekly cron job to purge expired keys older than 1 year - Add DeleteLicenseKey and DeleteExpiredKeys model functions Co-Authored-By: Claude Opus 4.6 (1M context) --- models/licenses/license_key.go | 14 ++++++++++++++ options/locale/locale_en-US.json | 3 +++ routers/web/org/licenses.go | 15 +++++++++++++++ routers/web/repo/licenses.go | 15 +++++++++++++++ routers/web/repo/updateserver.go | 14 ++++++++++++++ routers/web/web.go | 2 ++ services/cron/tasks_basic.go | 21 +++++++++++++++++++++ templates/org/licenses.tmpl | 5 +++++ templates/repo/licenses.tmpl | 5 +++++ 9 files changed, 94 insertions(+) diff --git a/models/licenses/license_key.go b/models/licenses/license_key.go index 827ff5ddf2..4e93a69bbd 100644 --- a/models/licenses/license_key.go +++ b/models/licenses/license_key.go @@ -155,6 +155,20 @@ func UpdateLicenseKey(ctx context.Context, key *LicenseKey) error { return err } +// DeleteLicenseKey permanently removes a license key by ID. +func DeleteLicenseKey(ctx context.Context, id int64) error { + _, err := db.GetEngine(ctx).ID(id).Delete(new(LicenseKey)) + return err +} + +// DeleteExpiredKeys removes keys that expired more than the given duration ago. +func DeleteExpiredKeys(ctx context.Context, olderThanDays int) (int64, error) { + cutoff := timeutil.TimeStampNow() - timeutil.TimeStamp(int64(olderThanDays)*86400) + return db.GetEngine(ctx). + Where("expires_unix > 0 AND expires_unix < ? AND is_internal = ?", cutoff, false). + Delete(new(LicenseKey)) +} + // TouchHeartbeat updates the last heartbeat timestamp for a key. func TouchHeartbeat(ctx context.Context, keyID int64) error { _, err := db.GetEngine(ctx).ID(keyID). diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 1ee6f8c4b5..020c0a6d3b 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -2686,6 +2686,9 @@ "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.licenses.delete_key": "Delete Key", + "repo.licenses.confirm_delete_key": "Permanently delete this license key? This cannot be undone.", + "repo.licenses.key_deleted": "License key deleted.", "repo.release.draft": "Draft", "repo.release.prerelease": "Pre-Release", "repo.release.stable": "Stable", diff --git a/routers/web/org/licenses.go b/routers/web/org/licenses.go index f51ac9ba83..95e80fa810 100644 --- a/routers/web/org/licenses.go +++ b/routers/web/org/licenses.go @@ -437,3 +437,18 @@ func LicensesRenewKey(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("repo.licenses.key_renewed", days)) ctx.Redirect(ctx.Org.OrgLink + "/-/licenses") } + +// LicensesDeleteKey permanently deletes a license key. Site admin only. +func LicensesDeleteKey(ctx *context.Context) { + if !ctx.IsUserSiteAdmin() { + ctx.NotFound(nil) + return + } + keyID := ctx.PathParamInt64("id") + if err := licenses.DeleteLicenseKey(ctx, keyID); err != nil { + ctx.ServerError("DeleteLicenseKey", err) + return + } + ctx.Flash.Success(ctx.Tr("repo.licenses.key_deleted")) + ctx.Redirect(ctx.Org.OrgLink + "/-/licenses") +} diff --git a/routers/web/repo/licenses.go b/routers/web/repo/licenses.go index d9d32dfe9f..28b34f9402 100644 --- a/routers/web/repo/licenses.go +++ b/routers/web/repo/licenses.go @@ -440,3 +440,18 @@ func LicensesRenewKey(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("repo.licenses.key_renewed", days)) ctx.Redirect(ctx.Repo.RepoLink + "/licenses") } + +// LicensesDeleteKey permanently deletes a license key. Site admin only. +func LicensesDeleteKey(ctx *context.Context) { + if !ctx.IsUserSiteAdmin() { + ctx.NotFound(nil) + return + } + keyID := ctx.PathParamInt64("id") + if err := licenses.DeleteLicenseKey(ctx, keyID); err != nil { + ctx.ServerError("DeleteLicenseKey", err) + return + } + ctx.Flash.Success(ctx.Tr("repo.licenses.key_deleted")) + ctx.Redirect(ctx.Repo.RepoLink + "/licenses") +} diff --git a/routers/web/repo/updateserver.go b/routers/web/repo/updateserver.go index 21cac850f4..265e96c683 100644 --- a/routers/web/repo/updateserver.go +++ b/routers/web/repo/updateserver.go @@ -81,6 +81,13 @@ func validateUpdateKey(ctx *context.Context) (allowedChannels []string, ok bool) // ServeUpdatesXML generates and serves a Joomla-compatible updates.xml // from the repository's releases. func ServeUpdatesXML(ctx *context.Context) { + // Block if platform doesn't include joomla. + platform := ctx.Data["RepoUpdatePlatform"] + if platform == "dolibarr" { + ctx.NotFound(nil) + return + } + allowedChannels, ok := validateUpdateKey(ctx) if !ok { // Return empty updates XML for invalid keys (Joomla-compatible). @@ -109,6 +116,13 @@ func ServeUpdatesXML(ctx *context.Context) { // from the repository's releases. Uses the same license key validation as the // Joomla XML feed — all platforms share the same licensing system. func ServeDolibarrJSON(ctx *context.Context) { + // Block if platform doesn't include dolibarr. + platform := ctx.Data["RepoUpdatePlatform"] + if platform == "joomla" || platform == "" { + ctx.NotFound(nil) + return + } + allowedChannels, ok := validateUpdateKey(ctx) if !ok { // Return empty updates for invalid keys. diff --git a/routers/web/web.go b/routers/web/web.go index 64675d081a..3e9461ba0b 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1116,6 +1116,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Post("/keys/{id}/edit", org.LicensesEditKeyPost) m.Post("/keys/{id}/revoke", org.LicensesRevokeKey) m.Post("/keys/{id}/renew", org.LicensesRenewKey) + m.Post("/keys/{id}/delete", org.LicensesDeleteKey) }, reqUnitAccess(unit.TypeLicenses, perm.AccessModeWrite, true)) }, reqUnitAccess(unit.TypeLicenses, perm.AccessModeRead, true)) @@ -1535,6 +1536,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Post("/keys/{id}/edit", repo.LicensesEditKeyPost) m.Post("/keys/{id}/revoke", repo.LicensesRevokeKey) m.Post("/keys/{id}/renew", repo.LicensesRenewKey) + m.Post("/keys/{id}/delete", repo.LicensesDeleteKey) }, optSignIn, context.RepoAssignment) // end "/{username}/{reponame}": licenses diff --git a/services/cron/tasks_basic.go b/services/cron/tasks_basic.go index c91249e06d..72cb21ff8b 100644 --- a/services/cron/tasks_basic.go +++ b/services/cron/tasks_basic.go @@ -9,9 +9,11 @@ import ( "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models" git_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git" + licenses_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" user_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user" "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/webhook" "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git/gitcmd" + "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/updatechecker" "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth" @@ -158,6 +160,24 @@ func registerCleanupPackages() { }) } +func registerCleanupExpiredLicenseKeys() { + RegisterTaskFatal("cleanup_expired_license_keys", &BaseConfig{ + Enabled: true, + RunAtStart: false, + Schedule: "@weekly", + }, func(ctx context.Context, _ *user_model.User, config Config) error { + // Delete non-internal keys that expired more than 365 days ago. + deleted, err := licenses_model.DeleteExpiredKeys(ctx, 365) + if err != nil { + return err + } + if deleted > 0 { + log.Info("Cleaned up %d expired license keys (expired >1 year)", deleted) + } + return nil + }) +} + func registerSyncRepoLicenses() { RegisterTaskFatal("sync_repo_licenses", &BaseConfig{ Enabled: false, @@ -185,6 +205,7 @@ func initBasicTasks() { registerCleanupPackages() } registerSyncRepoLicenses() + registerCleanupExpiredLicenseKeys() if setting.UpdateChecker.Enabled { registerUpdateChecker() } diff --git a/templates/org/licenses.tmpl b/templates/org/licenses.tmpl index 4f19318e7f..e2c2c1027f 100644 --- a/templates/org/licenses.tmpl +++ b/templates/org/licenses.tmpl @@ -168,6 +168,11 @@ + {{if $.IsSiteAdmin}} + + {{end}} {{end}} diff --git a/templates/repo/licenses.tmpl b/templates/repo/licenses.tmpl index 8364d208e6..6b94af8cac 100644 --- a/templates/repo/licenses.tmpl +++ b/templates/repo/licenses.tmpl @@ -174,6 +174,11 @@ + {{if $.IsSiteAdmin}} + + {{end}} {{end}}