Merge pull request 'feat(licenses): platform enforcement, key deletion, expired key cleanup' (#340) from dev into main
Deploy MokoGitea / deploy (push) Failing after 3m4s

This commit was merged in pull request #340.
This commit is contained in:
2026-05-31 15:03:46 +00:00
9 changed files with 100 additions and 0 deletions
+14
View File
@@ -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).
+3
View File
@@ -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",
+15
View File
@@ -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")
}
+15
View File
@@ -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")
}
+14
View File
@@ -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.
+2
View File
@@ -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
+21
View File
@@ -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()
}
+5
View File
@@ -168,6 +168,11 @@
<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>
{{if $.IsSiteAdmin}}
<button class="ui tiny red button link-action" data-url="{{$.Org.HomeLink}}/-/licenses/keys/{{.ID}}/delete" data-modal-confirm="{{ctx.Locale.Tr "repo.licenses.confirm_delete_key"}}" title="{{ctx.Locale.Tr "repo.licenses.delete_key"}}">
{{svg "octicon-trash" 14}}
</button>
{{end}}
</td>
{{end}}
</tr>
+11
View File
@@ -174,6 +174,11 @@
<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>
{{if $.IsSiteAdmin}}
<button class="ui tiny red button link-action" data-url="{{$.RepoLink}}/licenses/keys/{{.ID}}/delete" data-modal-confirm="{{ctx.Locale.Tr "repo.licenses.confirm_delete_key"}}" title="{{ctx.Locale.Tr "repo.licenses.delete_key"}}">
{{svg "octicon-trash" 14}}
</button>
{{end}}
</td>
{{end}}
</tr>
@@ -184,10 +189,12 @@
{{end}}
{{/* Update Feed URLs */}}
{{if .LicensingEnabled}}
<h4 class="ui top attached header tw-mt-4">
{{svg "octicon-rss" 16}} {{ctx.Locale.Tr "repo.licenses.update_feeds"}}
</h4>
<div class="ui attached segment">
{{if or (eq .RepoUpdatePlatform "joomla") (eq .RepoUpdatePlatform "both") (eq .RepoUpdatePlatform "")}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.feed_joomla_updates"}}</label>
<div class="ui action input tw-w-full">
@@ -195,6 +202,8 @@
<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>
{{end}}
{{if or (eq .RepoUpdatePlatform "dolibarr") (eq .RepoUpdatePlatform "both")}}
<div class="field tw-mt-2">
<label>{{ctx.Locale.Tr "repo.licenses.feed_dolibarr_updates"}}</label>
<div class="ui action input tw-w-full">
@@ -202,7 +211,9 @@
<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>
{{end}}
</div>
{{end}}
</div>
</div>
{{template "base/footer" .}}