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 e03d5dad11..6b94af8cac 100644 --- a/templates/repo/licenses.tmpl +++ b/templates/repo/licenses.tmpl @@ -174,6 +174,11 @@ + {{if $.IsSiteAdmin}} + + {{end}} {{end}} @@ -184,10 +189,12 @@ {{end}} {{/* ── Update Feed URLs ── */}} + {{if .LicensingEnabled}}