66e728b078
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Generic: Repo Health / Release configuration (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Access control (push) Successful in 18s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: Auto Version Bump / Version Bump (push) Failing after 27s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 28s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m7s
Auto-fixed 5006 tab-indent and line-ending errors via phpcbf, then manually broke 100 lines exceeding 150-char limit. All 74 files in cli/, automation/, maintenance/, deploy/ now pass PHPCS PSR-12 clean. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
687 lines
24 KiB
PHP
687 lines
24 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* FILE INFORMATION
|
|
* DEFGROUP: moko-platform.CLI
|
|
* INGROUP: moko-platform
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /cli/license_manage.php
|
|
* BRIEF: Manage license packages and keys via MokoGitea licensing API
|
|
*
|
|
* Usage:
|
|
* php bin/moko license:list --org MokoConsulting
|
|
* php bin/moko license:create-package --org MokoConsulting --name "Pro Annual" --duration 365 --max-sites 5
|
|
* php bin/moko license:issue --org MokoConsulting --package-id 1 --licensee "Client Inc" --email client@example.com
|
|
* php bin/moko license:revoke --org MokoConsulting --key-id 42
|
|
* php bin/moko license:renew --org MokoConsulting --key-id 42 --days 365
|
|
* php bin/moko license:validate --key MOKO-ABCD-1234-EF56-7890 --domain example.com
|
|
* php bin/moko license:usage --org MokoConsulting --key-id 42
|
|
* php bin/moko license:master-key --org MokoConsulting
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../vendor/autoload.php';
|
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
|
|
|
use MokoEnterprise\CliFramework;
|
|
|
|
class LicenseManage extends CliFramework
|
|
{
|
|
private string $apiBase = '';
|
|
private string $token = '';
|
|
private string $subcommand = '';
|
|
|
|
protected function configure(): void
|
|
{
|
|
$this->setDescription('Manage license packages and keys via MokoGitea licensing API');
|
|
$this->addArgument('--org', 'Organization name', '');
|
|
$this->addArgument('--api-base', 'Gitea API base URL', '');
|
|
$this->addArgument('--token', 'API token (or set GH_TOKEN env)', '');
|
|
|
|
// Package args
|
|
$this->addArgument('--name', 'Package name (for create-package)', '');
|
|
$this->addArgument('--description', 'Package description', '');
|
|
$this->addArgument('--duration', 'Duration in days (0 = lifetime)', '0');
|
|
$this->addArgument('--max-sites', 'Max sites per key (0 = unlimited)', '0');
|
|
$this->addArgument('--repo-scope', 'Repo scope: all or comma-separated repo IDs', 'all');
|
|
$this->addArgument('--channels', 'Allowed channels: JSON array or comma-separated', '');
|
|
|
|
// Key args
|
|
$this->addArgument('--package-id', 'License package ID', '');
|
|
$this->addArgument('--key-id', 'License key ID', '');
|
|
$this->addArgument('--key', 'Raw license key string (for validate)', '');
|
|
$this->addArgument('--licensee', 'Licensee name', '');
|
|
$this->addArgument('--email', 'Licensee email', '');
|
|
$this->addArgument('--domain', 'Domain restriction or validation domain', '');
|
|
$this->addArgument('--domains', 'Comma-separated allowed domains', '');
|
|
$this->addArgument('--payment-ref', 'Payment reference (idempotency key)', '');
|
|
$this->addArgument('--days', 'Days to extend (for renew)', '365');
|
|
$this->addArgument('--custom-key', 'Use a custom key string instead of auto-generated', '');
|
|
|
|
// Output
|
|
$this->addArgument('--json', 'Output as JSON', false);
|
|
}
|
|
|
|
protected function initialize(): void
|
|
{
|
|
// Resolve API base
|
|
$this->apiBase = $this->getArgument('--api-base')
|
|
?: getenv('GITEA_URL')
|
|
?: 'https://git.mokoconsulting.tech';
|
|
$this->apiBase = rtrim($this->apiBase, '/');
|
|
|
|
// Resolve token
|
|
$this->token = $this->getArgument('--token')
|
|
?: getenv('GH_TOKEN')
|
|
?: getenv('GITHUB_TOKEN')
|
|
?: '';
|
|
|
|
if (empty($this->token)) {
|
|
$ghToken = trim((string) @shell_exec('gh auth token 2>/dev/null'));
|
|
if (!empty($ghToken)) {
|
|
$this->token = $ghToken;
|
|
}
|
|
}
|
|
|
|
// Determine subcommand from argv
|
|
global $argv;
|
|
foreach ($argv as $arg) {
|
|
if (
|
|
in_array($arg, [
|
|
'list', 'create-package', 'update-package', 'delete-package',
|
|
'issue', 'revoke', 'activate', 'renew', 'validate',
|
|
'usage', 'master-key', 'keys', 'packages',
|
|
], true)
|
|
) {
|
|
$this->subcommand = $arg;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function run(): int
|
|
{
|
|
if (empty($this->token)) {
|
|
$this->log('No API token found. Set GH_TOKEN or pass --token.', 'ERROR');
|
|
return 1;
|
|
}
|
|
|
|
return match ($this->subcommand) {
|
|
'packages', 'list' => $this->listPackages(),
|
|
'create-package' => $this->createPackage(),
|
|
'update-package' => $this->updatePackage(),
|
|
'delete-package' => $this->deletePackage(),
|
|
'keys' => $this->listKeys(),
|
|
'issue' => $this->issueKey(),
|
|
'revoke' => $this->revokeKey(),
|
|
'activate' => $this->activateKey(),
|
|
'renew' => $this->renewKey(),
|
|
'validate' => $this->validateKey(),
|
|
'usage' => $this->viewUsage(),
|
|
'master-key' => $this->ensureMasterKey(),
|
|
default => $this->showSubcommandHelp(),
|
|
};
|
|
}
|
|
|
|
// ── Subcommand help ──────────────────────────────────────────────────
|
|
|
|
private function showSubcommandHelp(): int
|
|
{
|
|
$this->section('License Management — Subcommands');
|
|
echo <<<HELP
|
|
|
|
Package Management:
|
|
packages List all license packages for an org
|
|
create-package Create a new license package
|
|
update-package Update a license package (--package-id required)
|
|
delete-package Delete a license package (--package-id required)
|
|
|
|
Key Management:
|
|
keys List all license keys for an org
|
|
issue Issue a new license key (--package-id required)
|
|
revoke Deactivate a license key (--key-id required)
|
|
activate Re-activate a revoked key (--key-id required)
|
|
renew Extend key expiration (--key-id, --days required)
|
|
validate Validate a raw key string (--key required)
|
|
master-key Ensure master key exists for org
|
|
|
|
Analytics:
|
|
usage View usage logs for a key (--key-id required)
|
|
|
|
Examples:
|
|
php bin/moko license packages --org MokoConsulting
|
|
php bin/moko license create-package --org MokoConsulting --name "Pro Annual" --duration 365
|
|
php bin/moko license issue --org MokoConsulting --package-id 1 --licensee "Client"
|
|
php bin/moko license validate --key MOKO-ABCD-1234-EF56-7890 --domain example.com
|
|
php bin/moko license renew --org MokoConsulting --key-id 42 --days 365
|
|
|
|
HELP;
|
|
return 0;
|
|
}
|
|
|
|
// ── Package operations ───────────────────────────────────────────────
|
|
|
|
private function listPackages(): int
|
|
{
|
|
$org = $this->requireOrg();
|
|
if ($org === null) {
|
|
return 1;
|
|
}
|
|
|
|
$result = $this->apiGet("/orgs/{$org}/license-packages");
|
|
if ($result === null) {
|
|
return 1;
|
|
}
|
|
|
|
if ($this->getArgument('--json')) {
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
|
return 0;
|
|
}
|
|
|
|
$this->section("License Packages — {$org}");
|
|
if (empty($result)) {
|
|
$this->log('No packages found.', 'WARN');
|
|
return 0;
|
|
}
|
|
|
|
foreach ($result as $pkg) {
|
|
$duration = ($pkg['duration_days'] ?? 0) === 0 ? 'lifetime' : ($pkg['duration_days'] . ' days');
|
|
$sites = ($pkg['max_sites'] ?? 0) === 0 ? 'unlimited' : (string)$pkg['max_sites'];
|
|
$active = ($pkg['is_active'] ?? true) ? 'active' : 'inactive';
|
|
$this->status(
|
|
sprintf('#%d %s', $pkg['id'] ?? 0, $pkg['name'] ?? ''),
|
|
true,
|
|
sprintf('%s | %s sites | %s', $duration, $sites, $active)
|
|
);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
private function createPackage(): int
|
|
{
|
|
$org = $this->requireOrg();
|
|
if ($org === null) {
|
|
return 1;
|
|
}
|
|
|
|
$name = $this->getArgument('--name');
|
|
if (empty($name)) {
|
|
$this->log('--name is required for create-package', 'ERROR');
|
|
return 1;
|
|
}
|
|
|
|
$channels = $this->getArgument('--channels');
|
|
if (!empty($channels) && $channels[0] !== '[') {
|
|
$channels = json_encode(explode(',', $channels));
|
|
}
|
|
|
|
$data = [
|
|
'name' => $name,
|
|
'description' => $this->getArgument('--description') ?: '',
|
|
'duration_days' => (int) $this->getArgument('--duration'),
|
|
'max_sites' => (int) $this->getArgument('--max-sites'),
|
|
'repo_scope' => $this->getArgument('--repo-scope'),
|
|
'allowed_channels' => $channels ?: '',
|
|
];
|
|
|
|
if ($this->isDryRun()) {
|
|
$this->log('Would create package: ' . json_encode($data, JSON_PRETTY_PRINT), 'DRY-RUN');
|
|
return 0;
|
|
}
|
|
|
|
$result = $this->apiPost("/orgs/{$org}/license-packages", $data);
|
|
if ($result === null) {
|
|
return 1;
|
|
}
|
|
|
|
if ($this->getArgument('--json')) {
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
|
} else {
|
|
$this->log(sprintf('Created package #%d: %s', $result['id'] ?? 0, $name), 'OK');
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
private function updatePackage(): int
|
|
{
|
|
$org = $this->requireOrg();
|
|
$pkgId = $this->getArgument('--package-id');
|
|
if ($org === null || empty($pkgId)) {
|
|
$this->log('--org and --package-id are required', 'ERROR');
|
|
return 1;
|
|
}
|
|
|
|
$data = array_filter([
|
|
'name' => $this->getArgument('--name') ?: null,
|
|
'description' => $this->getArgument('--description') ?: null,
|
|
'duration_days' => $this->getArgument('--duration') !== '0' ? (int)$this->getArgument('--duration') : null,
|
|
'max_sites' => $this->getArgument('--max-sites') !== '0' ? (int)$this->getArgument('--max-sites') : null,
|
|
], fn($v) => $v !== null);
|
|
|
|
if (empty($data)) {
|
|
$this->log('No fields to update. Pass --name, --description, --duration, or --max-sites.', 'WARN');
|
|
return 1;
|
|
}
|
|
|
|
if ($this->isDryRun()) {
|
|
$this->log("Would update package #{$pkgId}: " . json_encode($data), 'DRY-RUN');
|
|
return 0;
|
|
}
|
|
|
|
$result = $this->apiPatch("/orgs/{$org}/license-packages/{$pkgId}", $data);
|
|
if ($result === null) {
|
|
return 1;
|
|
}
|
|
|
|
$this->log("Updated package #{$pkgId}", 'OK');
|
|
return 0;
|
|
}
|
|
|
|
private function deletePackage(): int
|
|
{
|
|
$org = $this->requireOrg();
|
|
$pkgId = $this->getArgument('--package-id');
|
|
if ($org === null || empty($pkgId)) {
|
|
$this->log('--org and --package-id are required', 'ERROR');
|
|
return 1;
|
|
}
|
|
|
|
if ($this->isDryRun()) {
|
|
$this->log("Would delete package #{$pkgId}", 'DRY-RUN');
|
|
return 0;
|
|
}
|
|
|
|
$result = $this->apiDelete("/orgs/{$org}/license-packages/{$pkgId}");
|
|
if ($result === null) {
|
|
return 1;
|
|
}
|
|
|
|
$this->log("Deleted package #{$pkgId}", 'OK');
|
|
return 0;
|
|
}
|
|
|
|
// ── Key operations ───────────────────────────────────────────────────
|
|
|
|
private function listKeys(): int
|
|
{
|
|
$org = $this->requireOrg();
|
|
if ($org === null) {
|
|
return 1;
|
|
}
|
|
|
|
$pkgId = $this->getArgument('--package-id');
|
|
$endpoint = $pkgId
|
|
? "/orgs/{$org}/license-packages/{$pkgId}/keys"
|
|
: "/orgs/{$org}/license-keys";
|
|
|
|
$result = $this->apiGet($endpoint);
|
|
if ($result === null) {
|
|
return 1;
|
|
}
|
|
|
|
if ($this->getArgument('--json')) {
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
|
return 0;
|
|
}
|
|
|
|
$this->section("License Keys — {$org}" . ($pkgId ? " (Package #{$pkgId})" : ''));
|
|
if (empty($result)) {
|
|
$this->log('No keys found.', 'WARN');
|
|
return 0;
|
|
}
|
|
|
|
foreach ($result as $key) {
|
|
$prefix = $key['key_prefix'] ?? '???';
|
|
$licensee = $key['licensee_name'] ?? 'N/A';
|
|
$active = ($key['is_active'] ?? true) ? 'active' : 'revoked';
|
|
$internal = ($key['is_internal'] ?? false) ? ' [MASTER]' : '';
|
|
$domains = $key['domain_restriction'] ?? '';
|
|
$expires = ($key['expires_unix'] ?? 0) > 0
|
|
? date('Y-m-d', (int) $key['expires_unix'])
|
|
: 'never';
|
|
|
|
$this->status(
|
|
sprintf('#%d %s', $key['id'] ?? 0, $prefix),
|
|
$key['is_active'] ?? true,
|
|
sprintf('%s | %s | expires: %s | domains: %s%s', $licensee, $active, $expires, $domains ?: 'any', $internal)
|
|
);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
private function issueKey(): int
|
|
{
|
|
$org = $this->requireOrg();
|
|
$pkgId = $this->getArgument('--package-id');
|
|
if ($org === null || empty($pkgId)) {
|
|
$this->log('--org and --package-id are required', 'ERROR');
|
|
return 1;
|
|
}
|
|
|
|
$data = [
|
|
'package_id' => (int) $pkgId,
|
|
'licensee_name' => $this->getArgument('--licensee') ?: '',
|
|
'licensee_email' => $this->getArgument('--email') ?: '',
|
|
'domain_restriction' => $this->getArgument('--domains') ?: '',
|
|
'max_sites' => (int) $this->getArgument('--max-sites'),
|
|
'payment_ref' => $this->getArgument('--payment-ref') ?: '',
|
|
];
|
|
|
|
$customKey = $this->getArgument('--custom-key');
|
|
if (!empty($customKey)) {
|
|
$data['custom_key'] = $customKey;
|
|
}
|
|
|
|
if ($this->isDryRun()) {
|
|
$this->log('Would issue key: ' . json_encode($data, JSON_PRETTY_PRINT), 'DRY-RUN');
|
|
return 0;
|
|
}
|
|
|
|
$result = $this->apiPost("/orgs/{$org}/license-keys", $data);
|
|
if ($result === null) {
|
|
return 1;
|
|
}
|
|
|
|
if ($this->getArgument('--json')) {
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
|
} else {
|
|
$rawKey = $result['raw_key'] ?? '';
|
|
$this->section('License Key Issued');
|
|
if (!empty($rawKey)) {
|
|
echo "\n";
|
|
$this->log("Raw Key: {$rawKey}", 'OK');
|
|
$this->log('This key will NOT be shown again. Save it now.', 'WARN');
|
|
echo "\n";
|
|
}
|
|
$this->log(sprintf('Key ID: #%d | Prefix: %s', $result['id'] ?? 0, $result['key_prefix'] ?? ''), 'INFO');
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
private function revokeKey(): int
|
|
{
|
|
return $this->toggleKey(false);
|
|
}
|
|
|
|
private function activateKey(): int
|
|
{
|
|
return $this->toggleKey(true);
|
|
}
|
|
|
|
private function toggleKey(bool $activate): int
|
|
{
|
|
$org = $this->requireOrg();
|
|
$keyId = $this->getArgument('--key-id');
|
|
if ($org === null || empty($keyId)) {
|
|
$this->log('--org and --key-id are required', 'ERROR');
|
|
return 1;
|
|
}
|
|
|
|
$action = $activate ? 'activate' : 'revoke';
|
|
|
|
if ($this->isDryRun()) {
|
|
$this->log("Would {$action} key #{$keyId}", 'DRY-RUN');
|
|
return 0;
|
|
}
|
|
|
|
$result = $this->apiPatch("/orgs/{$org}/license-keys/{$keyId}", [
|
|
'is_active' => $activate,
|
|
]);
|
|
if ($result === null) {
|
|
return 1;
|
|
}
|
|
|
|
$label = $activate ? 'Activated' : 'Revoked';
|
|
$this->log("{$label} key #{$keyId}", 'OK');
|
|
return 0;
|
|
}
|
|
|
|
private function renewKey(): int
|
|
{
|
|
$org = $this->requireOrg();
|
|
$keyId = $this->getArgument('--key-id');
|
|
$days = (int) $this->getArgument('--days');
|
|
if ($org === null || empty($keyId)) {
|
|
$this->log('--org and --key-id are required', 'ERROR');
|
|
return 1;
|
|
}
|
|
|
|
if ($this->isDryRun()) {
|
|
$this->log("Would renew key #{$keyId} by {$days} days", 'DRY-RUN');
|
|
return 0;
|
|
}
|
|
|
|
$result = $this->apiPost("/orgs/{$org}/license-keys/{$keyId}/renew", [
|
|
'days' => $days,
|
|
]);
|
|
if ($result === null) {
|
|
return 1;
|
|
}
|
|
|
|
$newExpiry = isset($result['expires_unix']) && $result['expires_unix'] > 0
|
|
? date('Y-m-d', (int) $result['expires_unix'])
|
|
: 'never';
|
|
$this->log("Renewed key #{$keyId} — new expiry: {$newExpiry}", 'OK');
|
|
return 0;
|
|
}
|
|
|
|
private function validateKey(): int
|
|
{
|
|
$rawKey = $this->getArgument('--key');
|
|
if (empty($rawKey)) {
|
|
$this->log('--key is required for validate', 'ERROR');
|
|
return 1;
|
|
}
|
|
|
|
$data = ['key' => $rawKey];
|
|
$domain = $this->getArgument('--domain');
|
|
if (!empty($domain)) {
|
|
$data['domain'] = $domain;
|
|
}
|
|
|
|
$result = $this->apiPost('/license-keys/validate', $data);
|
|
if ($result === null) {
|
|
return 1;
|
|
}
|
|
|
|
if ($this->getArgument('--json')) {
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
|
return 0;
|
|
}
|
|
|
|
$valid = $result['valid'] ?? false;
|
|
if ($valid) {
|
|
$this->status('License Valid', true, sprintf(
|
|
'Package: %s | Expires: %s | Sites: %s',
|
|
$result['package_name'] ?? 'N/A',
|
|
isset($result['expires_unix']) && $result['expires_unix'] > 0
|
|
? date('Y-m-d', (int) $result['expires_unix']) : 'never',
|
|
$result['max_sites'] ?? 'unlimited'
|
|
));
|
|
return 0;
|
|
} else {
|
|
$this->status('License Invalid', false, $result['error'] ?? 'Unknown reason');
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
private function viewUsage(): int
|
|
{
|
|
$org = $this->requireOrg();
|
|
$keyId = $this->getArgument('--key-id');
|
|
if ($org === null || empty($keyId)) {
|
|
$this->log('--org and --key-id are required', 'ERROR');
|
|
return 1;
|
|
}
|
|
|
|
$result = $this->apiGet("/orgs/{$org}/license-keys/{$keyId}/usage");
|
|
if ($result === null) {
|
|
return 1;
|
|
}
|
|
|
|
if ($this->getArgument('--json')) {
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
|
return 0;
|
|
}
|
|
|
|
$this->section("Usage — Key #{$keyId}");
|
|
$entries = $result['entries'] ?? $result;
|
|
if (empty($entries)) {
|
|
$this->log('No usage recorded.', 'WARN');
|
|
return 0;
|
|
}
|
|
|
|
foreach ($entries as $u) {
|
|
$date = isset($u['created_unix']) ? date('Y-m-d H:i', (int) $u['created_unix']) : 'N/A';
|
|
$domain = $u['domain'] ?? '';
|
|
$ip = $u['ip_address'] ?? '';
|
|
$from = $u['version_from'] ?? '';
|
|
$this->log(sprintf('%s | %s | %s | from %s', $date, $domain ?: 'no domain', $ip, $from ?: 'unknown'), 'INFO');
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
private function ensureMasterKey(): int
|
|
{
|
|
$org = $this->requireOrg();
|
|
if ($org === null) {
|
|
return 1;
|
|
}
|
|
|
|
if ($this->isDryRun()) {
|
|
$this->log("Would ensure master key for {$org}", 'DRY-RUN');
|
|
return 0;
|
|
}
|
|
|
|
$result = $this->apiPost("/orgs/{$org}/license-keys/master", []);
|
|
if ($result === null) {
|
|
return 1;
|
|
}
|
|
|
|
if ($this->getArgument('--json')) {
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
|
return 0;
|
|
}
|
|
|
|
$rawKey = $result['raw_key'] ?? '';
|
|
if (!empty($rawKey)) {
|
|
$this->section('Master Key Created');
|
|
echo "\n";
|
|
$this->log("Raw Key: {$rawKey}", 'OK');
|
|
$this->log('This key will NOT be shown again. Save it now.', 'WARN');
|
|
echo "\n";
|
|
} else {
|
|
$this->log('Master key already exists.', 'INFO');
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
|
|
private function requireOrg(): ?string
|
|
{
|
|
$org = $this->getArgument('--org');
|
|
if (empty($org)) {
|
|
// Try to detect from git remote
|
|
$remote = trim((string) @shell_exec('git remote get-url origin 2>/dev/null'));
|
|
if (preg_match('#[/:]([^/]+)/[^/]+?(?:\.git)?$#', $remote, $m)) {
|
|
$org = $m[1];
|
|
}
|
|
}
|
|
if (empty($org)) {
|
|
$this->log('--org is required (or must be detectable from git remote)', 'ERROR');
|
|
return null;
|
|
}
|
|
return $org;
|
|
}
|
|
|
|
private function apiGet(string $path): ?array
|
|
{
|
|
return $this->apiRequest('GET', $path);
|
|
}
|
|
|
|
private function apiPost(string $path, array $data): ?array
|
|
{
|
|
return $this->apiRequest('POST', $path, $data);
|
|
}
|
|
|
|
private function apiPatch(string $path, array $data): ?array
|
|
{
|
|
return $this->apiRequest('PATCH', $path, $data);
|
|
}
|
|
|
|
private function apiDelete(string $path): ?array
|
|
{
|
|
return $this->apiRequest('DELETE', $path);
|
|
}
|
|
|
|
private function apiRequest(string $method, string $path, ?array $data = null): ?array
|
|
{
|
|
$url = $this->apiBase . '/api/v1' . $path;
|
|
|
|
$ch = curl_init();
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_URL => $url,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_CUSTOMREQUEST => $method,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Authorization: token ' . $this->token,
|
|
'Content-Type: application/json',
|
|
'Accept: application/json',
|
|
],
|
|
CURLOPT_TIMEOUT => 30,
|
|
]);
|
|
|
|
if ($data !== null && in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
|
}
|
|
|
|
if ($this->getArgument('--verbose')) {
|
|
$this->log("{$method} {$url}", 'DEBUG');
|
|
}
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$error = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if (!empty($error)) {
|
|
$this->log("API error: {$error}", 'ERROR');
|
|
return null;
|
|
}
|
|
|
|
if ($httpCode === 404) {
|
|
$this->log("API endpoint not found: {$path}", 'ERROR');
|
|
$this->log('The licensing API may not be deployed yet. Check MokoGitea version.', 'WARN');
|
|
return null;
|
|
}
|
|
|
|
if ($httpCode === 204) {
|
|
return []; // success, no content
|
|
}
|
|
|
|
if ($httpCode >= 400) {
|
|
$body = json_decode((string) $response, true);
|
|
$msg = $body['message'] ?? $response;
|
|
$this->log("API error ({$httpCode}): {$msg}", 'ERROR');
|
|
return null;
|
|
}
|
|
|
|
$decoded = json_decode((string) $response, true);
|
|
if ($decoded === null && !empty($response)) {
|
|
$this->log('Failed to parse API response', 'ERROR');
|
|
return null;
|
|
}
|
|
|
|
return $decoded ?? [];
|
|
}
|
|
}
|
|
|
|
$app = new LicenseManage();
|
|
exit($app->execute());
|