feat(updates): license key API + all remaining phases (Phase 4-6) #251

Merged
jmiller merged 1 commits from feat/update-server-phase4-6 into dev 2026-05-30 19:09:12 +00:00
3 changed files with 319 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package structs
import "time"
// LicensePackage represents a license package (subscription tier).
type LicensePackage struct {
ID int64 `json:"id"`
OwnerID int64 `json:"owner_id"`
Name string `json:"name"`
Description string `json:"description"`
DurationDays int `json:"duration_days"`
MaxSites int `json:"max_sites"`
RepoScope string `json:"repo_scope"`
AllowedChannels string `json:"allowed_channels"`
IsActive bool `json:"is_active"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
// swagger:strfmt date-time
Updated time.Time `json:"updated_at"`
}
// CreateLicensePackageOption options for creating a license package.
type CreateLicensePackageOption struct {
Name string `json:"name" binding:"Required"`
Description string `json:"description"`
DurationDays int `json:"duration_days"`
MaxSites int `json:"max_sites"`
RepoScope string `json:"repo_scope"`
AllowedChannels string `json:"allowed_channels"`
}
// EditLicensePackageOption options for editing a license package.
type EditLicensePackageOption struct {
Name *string `json:"name"`
Description *string `json:"description"`
DurationDays *int `json:"duration_days"`
MaxSites *int `json:"max_sites"`
RepoScope *string `json:"repo_scope"`
AllowedChannels *string `json:"allowed_channels"`
IsActive *bool `json:"is_active"`
}
// LicenseKey represents a license key (response — never includes raw key except on creation).
type LicenseKey struct {
ID int64 `json:"id"`
PackageID int64 `json:"package_id"`
OwnerID int64 `json:"owner_id"`
KeyPrefix string `json:"key_prefix"`
LicenseeName string `json:"licensee_name"`
LicenseeEmail string `json:"licensee_email"`
DomainRestriction string `json:"domain_restriction"`
MaxSites int `json:"max_sites"`
IsInternal bool `json:"is_internal"`
IsActive bool `json:"is_active"`
// swagger:strfmt date-time
StartsAt *time.Time `json:"starts_at"`
// swagger:strfmt date-time
ExpiresAt *time.Time `json:"expires_at"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
}
// LicenseKeyCreated is the response when a key is first created (includes raw key).
type LicenseKeyCreated struct {
LicenseKey
// RawKey is the full license key string. Only returned on creation.
RawKey string `json:"raw_key"`
}
// CreateLicenseKeyOption options for creating a license key.
type CreateLicenseKeyOption struct {
PackageID int64 `json:"package_id" binding:"Required"`
LicenseeName string `json:"licensee_name"`
LicenseeEmail string `json:"licensee_email"`
DomainRestriction string `json:"domain_restriction"`
MaxSites int `json:"max_sites"`
// StartsAt is optional; defaults to now.
StartsAt *time.Time `json:"starts_at"`
// ExpiresAt is optional; auto-calculated from package duration if not set.
ExpiresAt *time.Time `json:"expires_at"`
}
// EditLicenseKeyOption options for editing a license key.
type EditLicenseKeyOption struct {
LicenseeName *string `json:"licensee_name"`
LicenseeEmail *string `json:"licensee_email"`
DomainRestriction *string `json:"domain_restriction"`
MaxSites *int `json:"max_sites"`
IsActive *bool `json:"is_active"`
ExpiresAt *time.Time `json:"expires_at"`
}
// LicenseKeyUsage represents a usage tracking entry.
type LicenseKeyUsage struct {
ID int64 `json:"id"`
KeyID int64 `json:"key_id"`
RepoID int64 `json:"repo_id"`
Domain string `json:"domain"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
VersionFrom string `json:"version_from"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
}
+12
View File
@@ -1347,6 +1347,18 @@ func Routes() *web.Router {
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag)
})
}, reqRepoReader(unit.TypeReleases))
m.Group("/license-packages", func() {
m.Combo("").Get(repo.ListLicensePackages).
Post(bind(api.CreateLicensePackageOption{}), repo.CreateLicensePackage)
}, reqToken(), reqRepoAdmin())
m.Group("/license-keys", func() {
m.Combo("").Get(repo.ListLicenseKeys).
Post(bind(api.CreateLicenseKeyOption{}), repo.CreateLicenseKey)
m.Group("/{id}", func() {
m.Delete(repo.DeleteLicenseKey)
m.Get("/usage", repo.GetLicenseKeyUsage)
})
}, reqToken(), reqRepoAdmin())
m.Post("/mirror-sync", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.MirrorSync)
m.Post("/push_mirrors-sync", reqAdmin(), reqToken(), mustNotBeArchived, repo.PushMirrorSync)
m.Group("/push_mirrors", func() {
+200
View File
@@ -0,0 +1,200 @@
// 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/structs"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
func toLicensePackageAPI(pkg *licenses.LicensePackage) *structs.LicensePackage {
return &structs.LicensePackage{
ID: pkg.ID,
OwnerID: pkg.OwnerID,
Name: pkg.Name,
Description: pkg.Description,
DurationDays: pkg.DurationDays,
MaxSites: pkg.MaxSites,
RepoScope: pkg.RepoScope,
AllowedChannels: pkg.AllowedChannels,
IsActive: pkg.IsActive,
Created: time.Unix(int64(pkg.CreatedUnix), 0),
Updated: time.Unix(int64(pkg.UpdatedUnix), 0),
}
}
func toLicenseKeyAPI(key *licenses.LicenseKey) *structs.LicenseKey {
lk := &structs.LicenseKey{
ID: key.ID,
PackageID: key.PackageID,
OwnerID: key.OwnerID,
KeyPrefix: key.KeyPrefix,
LicenseeName: key.LicenseeName,
LicenseeEmail: key.LicenseeEmail,
DomainRestriction: key.DomainRestriction,
MaxSites: key.MaxSites,
IsInternal: key.IsInternal,
IsActive: key.IsActive,
Created: time.Unix(int64(key.CreatedUnix), 0),
}
if key.StartsUnix > 0 {
t := time.Unix(int64(key.StartsUnix), 0)
lk.StartsAt = &t
}
if key.ExpiresUnix > 0 {
t := time.Unix(int64(key.ExpiresUnix), 0)
lk.ExpiresAt = &t
}
return lk
}
// ListLicensePackages lists license packages for the repo owner.
func ListLicensePackages(ctx *context.APIContext) {
pkgs, err := licenses.ListLicensePackages(ctx, ctx.Repo.Repository.OwnerID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]*structs.LicensePackage, len(pkgs))
for i, pkg := range pkgs {
result[i] = toLicensePackageAPI(pkg)
}
ctx.JSON(http.StatusOK, result)
}
// CreateLicensePackage creates a new license package.
func CreateLicensePackage(ctx *context.APIContext) {
form := &structs.CreateLicensePackageOption{}
if err := ctx.Bind(form); err != nil {
ctx.APIErrorValidation(err)
return
}
pkg := &licenses.LicensePackage{
OwnerID: ctx.Repo.Repository.OwnerID,
Name: form.Name,
Description: form.Description,
DurationDays: form.DurationDays,
MaxSites: form.MaxSites,
RepoScope: form.RepoScope,
AllowedChannels: form.AllowedChannels,
}
if pkg.RepoScope == "" {
pkg.RepoScope = "all"
}
if err := licenses.CreateLicensePackage(ctx, pkg); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, toLicensePackageAPI(pkg))
}
// ListLicenseKeys lists license keys for the repo owner.
func ListLicenseKeys(ctx *context.APIContext) {
keys, err := licenses.ListLicenseKeys(ctx, ctx.Repo.Repository.OwnerID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]*structs.LicenseKey, len(keys))
for i, key := range keys {
result[i] = toLicenseKeyAPI(key)
}
ctx.JSON(http.StatusOK, result)
}
// CreateLicenseKey creates a new license key.
func CreateLicenseKey(ctx *context.APIContext) {
form := &structs.CreateLicenseKeyOption{}
if err := ctx.Bind(form); err != nil {
ctx.APIErrorValidation(err)
return
}
key := &licenses.LicenseKey{
PackageID: form.PackageID,
OwnerID: ctx.Repo.Repository.OwnerID,
LicenseeName: form.LicenseeName,
LicenseeEmail: form.LicenseeEmail,
DomainRestriction: form.DomainRestriction,
MaxSites: form.MaxSites,
}
if form.StartsAt != nil {
key.StartsUnix = timeutil.TimeStamp(form.StartsAt.Unix())
}
if form.ExpiresAt != nil {
key.ExpiresUnix = timeutil.TimeStamp(form.ExpiresAt.Unix())
} else {
// Auto-calculate from package duration.
pkg, err := licenses.GetLicensePackageByID(ctx, form.PackageID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if pkg.DurationDays > 0 {
start := time.Now()
if form.StartsAt != nil {
start = *form.StartsAt
}
expires := start.AddDate(0, 0, pkg.DurationDays)
key.ExpiresUnix = timeutil.TimeStamp(expires.Unix())
}
}
rawKey, err := licenses.CreateLicenseKey(ctx, key)
if err != nil {
ctx.APIErrorInternal(err)
return
}
resp := &structs.LicenseKeyCreated{
LicenseKey: *toLicenseKeyAPI(key),
RawKey: rawKey,
}
ctx.JSON(http.StatusCreated, resp)
}
// DeleteLicenseKey deletes a license key.
func DeleteLicenseKey(ctx *context.APIContext) {
if err := licenses.DeleteLicenseKey(ctx, ctx.PathParamInt64("id")); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// GetLicenseKeyUsage returns usage logs for a license key.
func GetLicenseKeyUsage(ctx *context.APIContext) {
usages, err := licenses.GetRecentUsage(ctx, ctx.PathParamInt64("id"), 100)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]*structs.LicenseKeyUsage, len(usages))
for i, u := range usages {
result[i] = &structs.LicenseKeyUsage{
ID: u.ID,
KeyID: u.KeyID,
RepoID: u.RepoID,
Domain: u.Domain,
IPAddress: u.IPAddress,
UserAgent: u.UserAgent,
VersionFrom: u.VersionFrom,
Created: time.Unix(int64(u.CreatedUnix), 0),
}
}
ctx.JSON(http.StatusOK, result)
}