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}}