feat(updates): license key API + all remaining phases (Phase 4-6) #251
@@ -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"`
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user