Compare commits

...

36 Commits

Author SHA1 Message Date
jmiller d55da332cf Merge branch 'main' into dev
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 3s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 10s
Universal: Auto Version Bump / Version Bump (push) Failing after 9s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 32s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 5m29s
2026-05-31 03:44:16 +00:00
jmiller a04e237f17 Merge pull request 'feat(licenses): org settings, copyable keys, master keys' (#282) from feat/org-licensing into dev
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: Build & Release / Promote to RC (pull_request) Failing after 12s
PR RC Release / Build RC Release (pull_request) Successful in 28s
2026-05-31 03:44:06 +00:00
jmiller e7cc4c120f Merge branch 'dev' into feat/org-licensing
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m13s
2026-05-31 03:43:55 +00:00
jmiller aa54f3834e chore: sync updates.xml 05.13.00 from main [skip ci] 2026-05-31 03:38:14 +00:00
gitea-actions[bot] 4d93f23037 chore: update channels for 05.13.00 [skip ci] 2026-05-31 03:37:46 +00:00
gitea-actions[bot] d7e2ffd02b chore(release): build 05.13.00 [skip ci] 2026-05-31 03:37:12 +00:00
jmiller b9d81ca5c5 Merge pull request 'chore: merge dev into main — URL fix' (#281) from dev into main
Deploy MokoGitea / deploy (push) Successful in 4m34s
2026-05-31 03:36:12 +00:00
jmiller 59c62dc687 Merge branch 'main' into dev
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 5s
Universal: PR Check / Branch Policy (pull_request) Successful in 4s
Universal: PR Check / Validate PR (pull_request) Failing after 14s
Universal: Auto Version Bump / Version Bump (push) Failing after 13s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 4m8s
2026-05-31 03:35:42 +00:00
jmiller b14ffa083e Merge pull request 'fix(ui): full domain URL in update feed fields' (#280) from fix/update-feed-urls into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 3s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 20s
Universal: Build & Release / Promote to RC (pull_request) Failing after 30s
PR RC Release / Build RC Release (pull_request) Successful in 50s
2026-05-31 03:34:56 +00:00
Jonathan Miller 2cc57bbbbc fix(ui): show full domain URL in update feed copy fields
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Branch Policy Check / Verify merge target (pull_request) Successful in 3s
PR RC Release / Build RC Release (pull_request) Successful in 5s
Universal: PR Check / Validate PR (pull_request) Failing after 17s
Branch Cleanup / Delete merged branch (pull_request) Successful in 4s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m40s
Use .Repository.HTMLURL instead of AppSubUrl+RepoLink so the
copyable update feed URLs include the full domain name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 22:34:12 -05:00
jmiller 3cd7687c06 chore: sync updates.xml 05.12.00 from main [skip ci] 2026-05-31 03:32:48 +00:00
gitea-actions[bot] c3b2643b0c chore: update channels for 05.12.00 [skip ci] 2026-05-31 03:32:46 +00:00
gitea-actions[bot] 0159e567e2 chore(release): build 05.12.00 [skip ci] 2026-05-31 03:32:10 +00:00
jmiller f194b204b4 Merge pull request 'chore: merge dev into main — org licenses + master keys' (#279) from dev into main
Deploy MokoGitea / deploy (push) Successful in 5m50s
2026-05-31 03:31:24 +00:00
jmiller f118f084ce Merge branch 'main' into dev
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 8s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 35s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 5m30s
2026-05-31 03:31:09 +00:00
jmiller 2821c35326 Merge pull request 'feat(licenses): org licenses page + master keys + menu fixes' (#278) from feat/org-licensing into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Universal: Build & Release / Promote to RC (pull_request) Failing after 17s
PR RC Release / Build RC Release (pull_request) Successful in 29s
2026-05-31 03:30:59 +00:00
Jonathan Miller 5b02cf188e feat(licenses): org-level licenses page, master keys, and menu fixes
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m13s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Major licensing UI improvements:
- Org-level Licenses tab in org menu (visible to org members)
- Org-level Licenses page with full CRUD (packages, keys, revoke)
- Auto-created master key: when admin first visits Licenses page,
  a Master (Internal) package + key is auto-generated
- Master keys marked with orange "Master" badge in key list
- Revoking a master key auto-creates a new one on next visit
- Fixed "New Package" button toggle (was using tw-hidden class
  that didn't work, now uses style.display)
- IsRepoAdmin set as context data for template access
- Master keys have IsInternal=true, lifetime, all channels

Ref #239

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 22:30:33 -05:00
Jonathan Miller 689173ecab feat(licenses): auto-create master key for org/repo
When an admin first visits the Licenses page, a master license package
and key are automatically created:
- Master package: lifetime, unlimited, all channels, all repos
- Master key: IsInternal=true, never expires
- Raw key shown once with copy instructions
- If master key is revoked, a new one is created on next visit

The master key is always present — revoking it and revisiting the page
generates a fresh one.

Ref #239

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 22:26:24 -05:00
jmiller b2fe44fbc3 chore: sync updates.xml 05.11.00 from main [skip ci] 2026-05-31 03:17:54 +00:00
gitea-actions[bot] 0e89ef9944 chore: update channels for 05.11.00 [skip ci] 2026-05-31 03:17:53 +00:00
gitea-actions[bot] 522dadecf0 chore(release): build 05.11.00 [skip ci] 2026-05-31 03:17:24 +00:00
jmiller f1b9bb2f3d Merge pull request 'chore: merge dev into main — licenses tab fix v2' (#277) from dev into main
Deploy MokoGitea / deploy (push) Failing after 4m28s
2026-05-31 03:16:44 +00:00
jmiller 7bbaf218d5 Merge branch 'main' into dev
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 8s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 32s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 4m11s
2026-05-31 03:16:31 +00:00
jmiller 33a550f838 Merge pull request 'fix(ui): IsRepoAdmin for Licenses tab' (#276) from fix/licenses-tab-v2 into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
PR RC Release / Build RC Release (pull_request) Successful in 26s
2026-05-31 03:16:21 +00:00
Jonathan Miller e29ee5f91b fix(ui): set IsRepoAdmin context data for Licenses tab visibility
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m9s
The template couldn't call .Permission.IsAdmin() directly. Set
IsRepoAdmin as a context data variable so the template can use it.
Licenses tab now shows for repo admins even without packages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 22:15:39 -05:00
jmiller 984a99188e chore: sync updates.xml 05.10.00 from main [skip ci] 2026-05-31 03:07:16 +00:00
gitea-actions[bot] 92fc77a6d1 chore: update channels for 05.10.00 [skip ci] 2026-05-31 03:07:15 +00:00
gitea-actions[bot] ea411e09be chore(release): build 05.10.00 [skip ci] 2026-05-31 03:06:36 +00:00
jmiller 9b141b39c5 Merge pull request 'chore: merge dev into main — licenses tab fix' (#275) from dev into main
Deploy MokoGitea / deploy (push) Successful in 4m35s
chore: merge dev into main — licenses tab fix (#275)
2026-05-31 03:05:51 +00:00
jmiller 85e4356fce Merge branch 'main' into dev
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 3s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: Auto Version Bump / Version Bump (push) Failing after 13s
Universal: PR Check / Validate PR (pull_request) Failing after 13s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 34s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 4m13s
2026-05-31 03:05:36 +00:00
jmiller 1654181a9e Merge pull request 'fix(ui): show Licenses tab for admins always' (#274) from fix/licenses-tab-always-admin into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Universal: Build & Release / Promote to RC (pull_request) Failing after 14s
PR RC Release / Build RC Release (pull_request) Successful in 28s
2026-05-31 03:05:25 +00:00
Jonathan Miller 282ef8f3e7 fix(ui): show Licenses tab for repo admins even without packages
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m14s
The Licenses tab was hidden when no packages existed, making it
impossible for admins to find the page to create their first package.
Now shows for repo admins always, and for everyone when packages exist.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 22:04:44 -05:00
jmiller a34eb53b2a chore: sync updates.xml 05.09.00 from main [skip ci] 2026-05-31 02:47:56 +00:00
gitea-actions[bot] 75d53c11b4 chore: update channels for 05.09.00 [skip ci] 2026-05-31 02:47:52 +00:00
gitea-actions[bot] 8556314468 chore(release): build 05.09.00 [skip ci] 2026-05-31 02:47:16 +00:00
jmiller 22624d662c Merge pull request 'chore: merge dev into main — licenses UI, update server, visibility' (#273) from dev into main
Deploy MokoGitea / deploy (push) Successful in 5m5s
chore: merge dev into main — all features (#273)
2026-05-31 02:46:35 +00:00
13 changed files with 465 additions and 13 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<name>MokoGitea</name>
<org>MokoConsulting</org>
<description>Moko fork of Gitea — adding project board REST API endpoints and custom enhancements</description>
<version>05.08.00</version>
<version>05.13.00</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# VERSION: 05.08.00
# VERSION: 05.13.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+89
View File
@@ -0,0 +1,89 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package licenses
import (
"context"
"fmt"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
)
const (
MasterPackageName = "Master (Internal)"
MasterPackageDesc = "Auto-created master package with unlimited access to all channels."
)
// EnsureMasterKey ensures that a master license package and key exist for the given owner.
// Returns the master key's raw key string only if it was just created (empty string otherwise).
func EnsureMasterKey(ctx context.Context, ownerID int64) (rawKey string, err error) {
// Check if a master package already exists.
pkgs, err := ListLicensePackages(ctx, ownerID)
if err != nil {
return "", err
}
var masterPkg *LicensePackage
for _, pkg := range pkgs {
if pkg.Name == MasterPackageName {
masterPkg = pkg
break
}
}
// Create master package if it doesn't exist.
if masterPkg == nil {
masterPkg = &LicensePackage{
OwnerID: ownerID,
Name: MasterPackageName,
Description: MasterPackageDesc,
DurationDays: 0, // lifetime
MaxSites: 0, // unlimited
RepoScope: "all",
IsActive: true,
}
if err := CreateLicensePackage(ctx, masterPkg); err != nil {
return "", fmt.Errorf("CreateLicensePackage: %w", err)
}
}
// Check if a master key already exists for this package.
keys, err := ListLicenseKeysByPackage(ctx, masterPkg.ID)
if err != nil {
return "", err
}
for _, key := range keys {
if key.IsInternal {
return "", nil // already exists, don't return raw key
}
}
// Create the master key.
masterKey := &LicenseKey{
PackageID: masterPkg.ID,
OwnerID: ownerID,
IsInternal: true,
IsActive: true,
}
rawKey, err = CreateLicenseKey(ctx, masterKey)
if err != nil {
return "", fmt.Errorf("CreateLicenseKey: %w", err)
}
return rawKey, nil
}
// GetMasterKey returns the master key for an owner, if it exists.
func GetMasterKey(ctx context.Context, ownerID int64) (*LicenseKey, error) {
key := new(LicenseKey)
has, err := db.GetEngine(ctx).Where("owner_id = ? AND is_internal = ?", ownerID, true).Get(key)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return key, nil
}
+2
View File
@@ -2643,6 +2643,8 @@
"repo.licenses.key_created_copy": "Copy this key now. It will not be shown again.",
"repo.licenses.revoke": "Revoke",
"repo.licenses.key_revoked": "License key revoked.",
"repo.licenses.master_key_created": "Master License Key Created",
"repo.licenses.master_key_created_copy": "This is your organization master key with unlimited access to all update channels. Copy it now — it will not be shown again.",
"repo.licenses.update_feeds": "Update Feed URLs",
"repo.release.draft": "Draft",
"repo.release.prerelease": "Pre-Release",
+179
View File
@@ -0,0 +1,179 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package org
import (
"net/http"
"strconv"
"time"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplOrgLicenses templates.TplName = "org/licenses"
// LicensePackageDisplay is used in templates.
type LicensePackageDisplay struct {
*licenses.LicensePackage
KeyCount int64
Created time.Time
}
// Licenses shows the org-level license packages and keys.
func Licenses(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.licenses")
ctx.Data["IsLicensesPage"] = true
org := ctx.Org.Organization
ownerID := org.ID
// Auto-create master key if org owner.
if ctx.Org.IsOwner {
newMasterKey, err := licenses.EnsureMasterKey(ctx, ownerID)
if err != nil {
ctx.ServerError("EnsureMasterKey", err)
return
}
if newMasterKey != "" {
ctx.Data["NewMasterKey"] = newMasterKey
}
}
pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
if err != nil {
ctx.ServerError("ListLicensePackages", err)
return
}
var display []LicensePackageDisplay
for _, pkg := range pkgs {
count, _ := licenses.CountKeysByPackage(ctx, pkg.ID)
display = append(display, LicensePackageDisplay{
LicensePackage: pkg,
KeyCount: count,
Created: time.Unix(int64(pkg.CreatedUnix), 0),
})
}
ctx.Data["LicensePackages"] = display
keys, err := licenses.ListLicenseKeys(ctx, ownerID)
if err != nil {
ctx.ServerError("ListLicenseKeys", err)
return
}
ctx.Data["LicenseKeys"] = keys
ctx.Data["IsRepoAdmin"] = ctx.Org.IsOwner
ctx.HTML(http.StatusOK, tplOrgLicenses)
}
// LicensesCreatePackage handles POST to create a new org-level license package.
func LicensesCreatePackage(ctx *context.Context) {
name := ctx.FormString("name")
if name == "" {
ctx.Flash.Error("Package name is required")
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
return
}
durationDays, _ := strconv.Atoi(ctx.FormString("duration_days"))
maxSites, _ := strconv.Atoi(ctx.FormString("max_sites"))
pkg := &licenses.LicensePackage{
OwnerID: ctx.Org.Organization.ID,
Name: name,
Description: ctx.FormString("description"),
DurationDays: durationDays,
MaxSites: maxSites,
AllowedChannels: ctx.FormString("allowed_channels"),
RepoScope: "all",
IsActive: true,
}
if err := licenses.CreateLicensePackage(ctx, pkg); err != nil {
ctx.ServerError("CreateLicensePackage", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.licenses.package_created"))
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
}
// LicensesGenerateKey handles POST to generate a key from an org package.
func LicensesGenerateKey(ctx *context.Context) {
packageID, _ := strconv.ParseInt(ctx.FormString("package_id"), 10, 64)
if packageID == 0 {
ctx.Flash.Error("Invalid package")
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
return
}
pkg, err := licenses.GetLicensePackageByID(ctx, packageID)
if err != nil {
ctx.ServerError("GetLicensePackageByID", err)
return
}
key := &licenses.LicenseKey{
PackageID: packageID,
OwnerID: ctx.Org.Organization.ID,
IsActive: true,
}
if pkg.DurationDays > 0 {
expires := time.Now().AddDate(0, 0, pkg.DurationDays)
key.ExpiresUnix = timeutil.TimeStamp(expires.Unix())
}
rawKey, err := licenses.CreateLicenseKey(ctx, key)
if err != nil {
ctx.ServerError("CreateLicenseKey", err)
return
}
// Re-render with the new key shown.
ctx.Data["Title"] = ctx.Tr("repo.licenses")
ctx.Data["IsLicensesPage"] = true
ctx.Data["IsRepoAdmin"] = ctx.Org.IsOwner
ctx.Data["NewKeyCreated"] = rawKey
ownerID := ctx.Org.Organization.ID
pkgs, _ := licenses.ListLicensePackages(ctx, ownerID)
var display []LicensePackageDisplay
for _, p := range pkgs {
count, _ := licenses.CountKeysByPackage(ctx, p.ID)
display = append(display, LicensePackageDisplay{
LicensePackage: p,
KeyCount: count,
Created: time.Unix(int64(p.CreatedUnix), 0),
})
}
ctx.Data["LicensePackages"] = display
keys, _ := licenses.ListLicenseKeys(ctx, ownerID)
ctx.Data["LicenseKeys"] = keys
ctx.HTML(http.StatusOK, tplOrgLicenses)
}
// LicensesRevokeKey handles POST to revoke an org license key.
func LicensesRevokeKey(ctx *context.Context) {
keyID := ctx.PathParamInt64("id")
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
if err != nil {
ctx.ServerError("GetLicenseKeyByID", err)
return
}
key.IsActive = false
if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
ctx.ServerError("UpdateLicenseKey", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.licenses.key_revoked"))
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
}
+12
View File
@@ -32,6 +32,18 @@ func Licenses(ctx *context.Context) {
ownerID := ctx.Repo.Repository.OwnerID
// Auto-create master package + key if admin and none exist.
if ctx.Repo.Permission.IsAdmin() {
newMasterKey, err := licenses.EnsureMasterKey(ctx, ownerID)
if err != nil {
ctx.ServerError("EnsureMasterKey", err)
return
}
if newMasterKey != "" {
ctx.Data["NewMasterKey"] = newMasterKey
}
}
pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
if err != nil {
ctx.ServerError("ListLicensePackages", err)
+7
View File
@@ -1099,6 +1099,13 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
// at the moment, only editing "owner-level projects" need to "mention", maybe in the future we can relax the permission check
m.Get("/mentions-in-owner", reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), org.GetMentionsInOwner)
m.Group("/licenses", func() {
m.Get("", org.Licenses)
m.Post("/packages", org.LicensesCreatePackage)
m.Post("/keys/generate", org.LicensesGenerateKey)
m.Post("/keys/{id}/revoke", org.LicensesRevokeKey)
})
m.Get("/repositories", org.Repositories)
m.Get("/heatmap", user.DashboardHeatmap)
+1
View File
@@ -612,6 +612,7 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare
})
ctx.Data["NumLicensePackages"] = numLicensePackages
ctx.Data["EnableLicenses"] = numLicensePackages > 0
ctx.Data["IsRepoAdmin"] = ctx.Repo.Permission.IsAdmin()
ctx.Data["Title"] = repo.Owner.Name + "/" + repo.Name
ctx.Data["PageTitleCommon"] = repo.Name + " - " + setting.AppName
+149
View File
@@ -0,0 +1,149 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content organization">
{{template "org/header" .}}
<div class="ui container">
{{if .NewMasterKey}}
<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>
{{end}}
{{if .NewKeyCreated}}
<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>
{{end}}
<h4 class="ui top attached header tw-flex tw-justify-between tw-items-center">
<span>{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}</span>
{{if .IsRepoAdmin}}
<a class="ui small primary button" href="#new-package-form" onclick="this.parentElement.parentElement.nextElementSibling.querySelector('#new-package-form').style.display='block'; return false;">
{{ctx.Locale.Tr "repo.licenses.new_package"}}
</a>
{{end}}
</h4>
<div class="ui attached segment">
{{if .IsRepoAdmin}}
<div id="new-package-form" style="display: none;" class="tw-mb-4">
<form class="ui form" method="post" action="{{$.Org.HomeLink}}/-/licenses/packages">
{{.CsrfTokenHtml}}
<div class="two fields">
<div class="required field">
<label>{{ctx.Locale.Tr "repo.licenses.package_name"}}</label>
<input name="name" required placeholder="Pro Annual">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.description"}}</label>
<input name="description" placeholder="Annual pro subscription">
</div>
</div>
<div class="three fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.duration"}} ({{ctx.Locale.Tr "repo.licenses.days"}})</label>
<input name="duration_days" type="number" value="0" min="0">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.max_sites"}}</label>
<input name="max_sites" type="number" value="0" min="0">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.channels"}}</label>
<input name="allowed_channels" placeholder="stable,release-candidate">
</div>
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.licenses.create_package"}}</button>
</form>
<div class="divider"></div>
</div>
{{end}}
{{if .LicensePackages}}
<table class="ui table">
<thead>
<tr>
<th>{{ctx.Locale.Tr "repo.licenses.package_name"}}</th>
<th>{{ctx.Locale.Tr "repo.licenses.duration"}}</th>
<th>{{ctx.Locale.Tr "repo.licenses.channels"}}</th>
<th>{{ctx.Locale.Tr "repo.licenses.keys_issued"}}</th>
<th>{{ctx.Locale.Tr "repo.licenses.status"}}</th>
{{if .IsRepoAdmin}}<th></th>{{end}}
</tr>
</thead>
<tbody>
{{range .LicensePackages}}
<tr>
<td><strong>{{.Name}}</strong>{{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">
<form method="post" action="{{$.Org.HomeLink}}/-/licenses/keys/generate" class="tw-inline">
{{$.CsrfTokenHtml}}
<input type="hidden" name="package_id" value="{{.ID}}">
<button class="ui tiny primary button" type="submit">
{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.generate_key"}}
</button>
</form>
</td>
{{end}}
</tr>
{{end}}
</tbody>
</table>
{{else}}
<div class="empty-placeholder">
{{svg "octicon-key" 48}}
<h2>{{ctx.Locale.Tr "repo.licenses.none"}}</h2>
<p>{{ctx.Locale.Tr "repo.licenses.none_desc"}}</p>
</div>
{{end}}
</div>
{{if .LicenseKeys}}
<h4 class="ui top attached header tw-mt-4">
{{svg "octicon-lock" 16}} {{ctx.Locale.Tr "repo.licenses.issued_keys"}}
</h4>
<div class="ui attached segment">
<table class="ui table">
<thead>
<tr>
<th>{{ctx.Locale.Tr "repo.licenses.key_prefix"}}</th>
<th>{{ctx.Locale.Tr "repo.licenses.licensee"}}</th>
<th>{{ctx.Locale.Tr "repo.licenses.expires"}}</th>
<th>{{ctx.Locale.Tr "repo.licenses.status"}}</th>
{{if .IsRepoAdmin}}<th></th>{{end}}
</tr>
</thead>
<tbody>
{{range .LicenseKeys}}
<tr>
<td><code>{{.KeyPrefix}}</code>{{if .IsInternal}} <span class="ui tiny orange label">Master</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.TimeSince .ExpiresUnix}}{{end}}</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">
<form method="post" action="{{$.Org.HomeLink}}/-/licenses/keys/{{.ID}}/revoke" class="tw-inline">
{{$.CsrfTokenHtml}}
<button class="ui tiny red button" type="submit">
{{svg "octicon-x" 14}}
</button>
</form>
</td>
{{end}}
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
</div>
</div>
{{template "base/footer" .}}
+5
View File
@@ -25,6 +25,11 @@
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
</a>
{{end}}
{{if .IsOrganizationMember}}
<a class="{{if .IsLicensesPage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/licenses">
{{svg "octicon-key"}} {{ctx.Locale.Tr "repo.licenses"}}
</a>
{{end}}
{{if and .IsRepoIndexerEnabled .CanReadCode}}
<a class="{{if .IsCodePage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/code">
{{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}}
+1 -1
View File
@@ -128,7 +128,7 @@
</a>
{{end}}
{{if .EnableLicenses}}
{{if or .EnableLicenses .IsRepoAdmin}}
<a href="{{.RepoLink}}/licenses" class="{{if .IsLicensesPage}}active {{end}}item">
{{svg "octicon-key"}} {{ctx.Locale.Tr "repo.licenses"}}
{{if .NumLicensePackages}}
+13 -5
View File
@@ -3,6 +3,14 @@
{{template "repo/header" .}}
<div class="ui container">
{{if .NewMasterKey}}
<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>
{{end}}
{{if .NewKeyCreated}}
<div class="ui success message">
<div class="header">{{ctx.Locale.Tr "repo.licenses.key_created"}}</div>
@@ -15,14 +23,14 @@
<h4 class="ui top attached header tw-flex tw-justify-between tw-items-center">
<span>{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}</span>
{{if .IsRepoAdmin}}
<button class="ui small primary button" onclick="document.getElementById('new-package-form').classList.toggle('tw-hidden')">
<a class="ui small primary button" href="#new-package-form" onclick="document.getElementById('new-package-form').style.display=document.getElementById('new-package-form').style.display==='none'?'block':'none'; return false;">
{{ctx.Locale.Tr "repo.licenses.new_package"}}
</button>
</a>
{{end}}
</h4>
<div class="ui attached segment">
{{if .IsRepoAdmin}}
<div id="new-package-form" class="tw-hidden tw-mb-4">
<div id="new-package-form" style="display: none;" class="tw-mb-4">
<form class="ui form" method="post" action="{{.RepoLink}}/licenses/packages">
{{.CsrfTokenHtml}}
<div class="two fields">
@@ -148,14 +156,14 @@
<div class="field">
<label>Joomla updates.xml</label>
<div class="ui action input tw-w-full">
<input type="text" readonly value="{{AppSubUrl}}{{.RepoLink}}/updates.xml" onclick="this.select()">
<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>
</div>
</div>
<div class="field tw-mt-2">
<label>Dolibarr JSON</label>
<div class="ui action input tw-w-full">
<input type="text" readonly value="{{AppSubUrl}}{{.RepoLink}}/updates/dolibarr.json" onclick="this.select()">
<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>
</div>
</div>
+5 -5
View File
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 05.08.00
VERSION: 05.13.00
-->
<updates>
@@ -87,13 +87,13 @@
<element>mokogitea</element>
<type>application</type>
<client>site</client>
<version>05.05.00</version>
<creationDate>2026-05-30</creationDate>
<version>05.13.00</version>
<creationDate>2026-05-31</creationDate>
<infourl title='MokoGitea'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/stable</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/stable/mokogitea-05.05.00.zip</downloadurl>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/stable/mokogitea-05.13.00.zip</downloadurl>
</downloads>
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
<sha256>b8d32fb99a0fde25ea0860b55ac5f3dfb8a15576863d9ddb80e67c5895722dcd</sha256>
<tags><tag>stable</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>