feat(licenses): add Licenses tab, page, and stream config #267
@@ -113,6 +113,11 @@ func ListLicenseKeysByPackage(ctx context.Context, packageID int64) ([]*LicenseK
|
||||
return keys, db.GetEngine(ctx).Where("package_id = ?", packageID).Find(&keys)
|
||||
}
|
||||
|
||||
// CountKeysByPackage returns the number of keys for a package.
|
||||
func CountKeysByPackage(ctx context.Context, packageID int64) (int64, error) {
|
||||
return db.GetEngine(ctx).Where("package_id = ?", packageID).Count(new(LicenseKey))
|
||||
}
|
||||
|
||||
// UpdateLicenseKey updates a license key.
|
||||
func UpdateLicenseKey(ctx context.Context, key *LicenseKey) error {
|
||||
_, err := db.GetEngine(ctx).ID(key.ID).AllCols().Update(key)
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -55,6 +57,20 @@ func GetLicensePackageByID(ctx context.Context, id int64) (*LicensePackage, erro
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
// FindLicensePackageOptions for db.Find/db.Count.
|
||||
type FindLicensePackageOptions struct {
|
||||
db.ListOptions
|
||||
OwnerID int64
|
||||
}
|
||||
|
||||
func (opts FindLicensePackageOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.OwnerID > 0 {
|
||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
// ListLicensePackages returns all packages for the given owner.
|
||||
func ListLicensePackages(ctx context.Context, ownerID int64) ([]*LicensePackage, error) {
|
||||
pkgs := make([]*LicensePackage, 0, 10)
|
||||
|
||||
@@ -2613,6 +2613,25 @@
|
||||
"repo.release.tags": "Tags",
|
||||
"repo.release.new_release": "New Release",
|
||||
"repo.release.update_feed": "Update Feed",
|
||||
"repo.licenses": "Licenses",
|
||||
"repo.licenses.packages": "License Packages",
|
||||
"repo.licenses.package_name": "Package",
|
||||
"repo.licenses.duration": "Duration",
|
||||
"repo.licenses.channels": "Channels",
|
||||
"repo.licenses.keys_issued": "Keys",
|
||||
"repo.licenses.status": "Status",
|
||||
"repo.licenses.lifetime": "Lifetime",
|
||||
"repo.licenses.days": "days",
|
||||
"repo.licenses.all_channels": "All channels",
|
||||
"repo.licenses.active": "Active",
|
||||
"repo.licenses.inactive": "Inactive",
|
||||
"repo.licenses.none": "No License Packages",
|
||||
"repo.licenses.none_desc": "License packages can be created via the API to gate access to update streams.",
|
||||
"repo.licenses.issued_keys": "Issued Keys",
|
||||
"repo.licenses.key_prefix": "Key",
|
||||
"repo.licenses.licensee": "Licensee",
|
||||
"repo.licenses.expires": "Expires",
|
||||
"repo.licenses.never": "Never",
|
||||
"repo.release.draft": "Draft",
|
||||
"repo.release.prerelease": "Pre-Release",
|
||||
"repo.release.stable": "Stable",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
const tplLicenses templates.TplName = "repo/licenses"
|
||||
|
||||
// LicensePackageDisplay is used in templates.
|
||||
type LicensePackageDisplay struct {
|
||||
*licenses.LicensePackage
|
||||
KeyCount int64
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
// Licenses shows the license packages and keys for a repo.
|
||||
func Licenses(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.licenses")
|
||||
ctx.Data["PageIsLicenses"] = true
|
||||
ctx.Data["IsLicensesPage"] = true
|
||||
|
||||
ownerID := ctx.Repo.Repository.OwnerID
|
||||
|
||||
pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicensePackages", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Build display list with key counts.
|
||||
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
|
||||
|
||||
// List all keys for the owner.
|
||||
keys, err := licenses.ListLicenseKeys(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicenseKeys", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["LicenseKeys"] = keys
|
||||
|
||||
ctx.HTML(http.StatusOK, tplLicenses)
|
||||
}
|
||||
@@ -1501,6 +1501,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
// end "/{username}/{reponame}": update server
|
||||
|
||||
// "/{username}/{reponame}": licenses page
|
||||
m.Group("/{username}/{reponame}", func() {
|
||||
m.Get("/licenses", repo.Licenses)
|
||||
}, reqSignIn, context.RepoAssignment, reqRepoReleaseReader)
|
||||
// end "/{username}/{reponame}": licenses
|
||||
|
||||
m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments
|
||||
m.Get("/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
git_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
|
||||
issues_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
||||
licenses_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
access_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm/access"
|
||||
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
unit_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
|
||||
@@ -605,6 +606,13 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare
|
||||
return
|
||||
}
|
||||
|
||||
// Check if license packages exist for this repo's owner (enables Licenses tab).
|
||||
numLicensePackages, _ := db.Count[licenses_model.LicensePackage](ctx, licenses_model.FindLicensePackageOptions{
|
||||
OwnerID: repo.OwnerID,
|
||||
})
|
||||
ctx.Data["NumLicensePackages"] = numLicensePackages
|
||||
ctx.Data["EnableLicenses"] = numLicensePackages > 0
|
||||
|
||||
ctx.Data["Title"] = repo.Owner.Name + "/" + repo.Name
|
||||
ctx.Data["PageTitleCommon"] = repo.Name + " - " + setting.AppName
|
||||
ctx.Data["Repository"] = repo
|
||||
|
||||
@@ -128,6 +128,15 @@
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if .EnableLicenses}}
|
||||
<a href="{{.RepoLink}}/licenses" class="{{if .IsLicensesPage}}active {{end}}item">
|
||||
{{svg "octicon-key"}} {{ctx.Locale.Tr "repo.licenses"}}
|
||||
{{if .NumLicensePackages}}
|
||||
<span class="ui small label">{{CountFmt .NumLicensePackages}}</span>
|
||||
{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{$projectsUnit := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeProjects}}
|
||||
{{if and (not ctx.Consts.RepoUnitTypeProjects.UnitGlobalDisabled) (.Permission.CanRead ctx.Consts.RepoUnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}}
|
||||
<a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item">
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository">
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
<h4 class="ui top attached header">
|
||||
{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
{{if .LicensePackages}}
|
||||
<h5>{{ctx.Locale.Tr "repo.licenses.packages"}}</h5>
|
||||
<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>
|
||||
</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>
|
||||
</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}}
|
||||
|
||||
{{if .LicenseKeys}}
|
||||
<div class="divider"></div>
|
||||
<h5>{{ctx.Locale.Tr "repo.licenses.issued_keys"}}</h5>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .LicenseKeys}}
|
||||
<tr>
|
||||
<td><code>{{.KeyPrefix}}</code></td>
|
||||
<td>{{.LicenseeName}}{{if .LicenseeEmail}} <small>({{.LicenseeEmail}})</small>{{end}}</td>
|
||||
<td>{{if eq .ExpiresUnix 0}}{{ctx.Locale.Tr "repo.licenses.never"}}{{else}}{{DateTime "short" (.ExpiresUnix.AsTime)}}{{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>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
Reference in New Issue
Block a user