new changes
This commit is contained in:
16
.env.example
Normal file
16
.env.example
Normal file
@@ -0,0 +1,16 @@
|
||||
POSTGRES_USER=yakpanel
|
||||
POSTGRES_PASSWORD=yakpanel_dev_password
|
||||
POSTGRES_DB=yakpanel
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
REDIS_PORT=6379
|
||||
|
||||
NATS_PORT=4222
|
||||
NATS_MONITOR_PORT=8222
|
||||
|
||||
MINIO_ROOT_USER=yakpanel
|
||||
MINIO_ROOT_PASSWORD=yakpanel_minio_password
|
||||
MINIO_PORT=9000
|
||||
MINIO_CONSOLE_PORT=9001
|
||||
|
||||
API_PORT=8080
|
||||
28
Makefile
Normal file
28
Makefile
Normal file
@@ -0,0 +1,28 @@
|
||||
SHELL := /bin/bash
|
||||
|
||||
COMPOSE := docker compose --env-file .env -f docker-compose.yakpanel.yml
|
||||
|
||||
.PHONY: up down logs ps init doctor migrate
|
||||
|
||||
init:
|
||||
cp -n .env.example .env || true
|
||||
@echo "Initialized .env (kept existing values if present)"
|
||||
|
||||
up:
|
||||
$(COMPOSE) up -d --build
|
||||
|
||||
down:
|
||||
$(COMPOSE) down
|
||||
|
||||
logs:
|
||||
$(COMPOSE) logs -f --tail=100
|
||||
|
||||
ps:
|
||||
$(COMPOSE) ps
|
||||
|
||||
doctor:
|
||||
@docker --version
|
||||
@docker compose version
|
||||
|
||||
migrate:
|
||||
$(COMPOSE) run --rm db-migrate
|
||||
28
README.md
28
README.md
@@ -91,3 +91,31 @@ Dir usage analysis
|
||||
**Note: after the deployment is complete, please immediately modify the user name and password in the panel settings and add the installation entry**
|
||||
|
||||
|
||||
## YakPanel 2026 Dev Scaffold (Ubuntu)
|
||||
|
||||
Use this for the new multi-service architecture scaffold in this repository:
|
||||
|
||||
```bash
|
||||
cd YakPanel-master
|
||||
chmod +x scripts/install-ubuntu.sh scripts/bootstrap-dev.sh
|
||||
./scripts/install-ubuntu.sh
|
||||
```
|
||||
|
||||
One-click installer:
|
||||
|
||||
```bash
|
||||
cd YakPanel-master
|
||||
chmod +x one-click-installer.sh
|
||||
./one-click-installer.sh
|
||||
```
|
||||
|
||||
Or run manually:
|
||||
|
||||
```bash
|
||||
make init
|
||||
make up
|
||||
make ps
|
||||
```
|
||||
|
||||
Reference: `docs/ubuntu-dev-install.md`
|
||||
|
||||
|
||||
64
architecture/2026/01-bounded-contexts.md
Normal file
64
architecture/2026/01-bounded-contexts.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# YakPanel 2026 Bounded Contexts and Ownership
|
||||
|
||||
This document defines ownership boundaries between the Laravel control plane and Go execution services.
|
||||
|
||||
## Domain Contexts
|
||||
|
||||
### IdentityAndAccess (Laravel)
|
||||
- Owns tenants, users, membership, roles, permissions, and scoped policy evaluation.
|
||||
- Exposes authn/authz services to all modules.
|
||||
- Guarantees tenant isolation at query and policy layers.
|
||||
|
||||
### TenantBillingAndLifecycle (Laravel)
|
||||
- Owns tenant lifecycle, plan limits, billing integration hooks, and quota enforcement.
|
||||
- Emits events used by job orchestration and plugin entitlements.
|
||||
|
||||
### InventoryAndServerRegistry (Laravel + agent-gateway)
|
||||
- Laravel owns canonical server records, labels, region mapping, and assignment.
|
||||
- `agent-gateway` owns live session presence and online/offline detection.
|
||||
|
||||
### HostingResourceManagement (Laravel)
|
||||
- Owns site, domain, SSL metadata, FTP metadata, database metadata, and lifecycle workflows.
|
||||
- Delegates mutable node operations to Go engines through command orchestration.
|
||||
|
||||
### WorkloadOrchestration (Laravel + Go engines)
|
||||
- Laravel owns workflow composition, step state machine, retry policy, audit trail.
|
||||
- Go services own command execution logic for privileged operations.
|
||||
|
||||
### PluginMarketplace (Laravel + Go engine-fileops/engine-docker)
|
||||
- Laravel owns catalog, signatures, compatibility metadata, entitlements.
|
||||
- Go services own installation actions on managed nodes.
|
||||
|
||||
### ObservabilityAndAlerting (Laravel + metrics pipeline)
|
||||
- Laravel owns dashboards, alert rules, routing, and incident metadata.
|
||||
- Metrics ingestion pipeline owns aggregation and retention.
|
||||
|
||||
### IntegrationAPI (Laravel)
|
||||
- Owns third-party API tokens, webhooks, and scoped public endpoints.
|
||||
|
||||
## Service Ownership Matrix
|
||||
|
||||
| Capability | Laravel Module | Go Service | Notes |
|
||||
|---|---|---|---|
|
||||
| Tenant and RBAC | Auth, Tenant, Rbac | N/A | Policy checks happen before dispatch. |
|
||||
| Server enrollment metadata | Server | agent-gateway | Enrollment token issued by Laravel, redeemed via gateway. |
|
||||
| Website lifecycle | Site, Domain, Ssl | engine-site | Laravel stores desired state; engine enforces actual state. |
|
||||
| Docker app deployment | Apps | engine-docker | Templates validated in Laravel, executed by engine-docker. |
|
||||
| MySQL/Redis management | Database, Redis | engine-db | Credentials references stored in Laravel. |
|
||||
| File operations | Files | engine-fileops | Strict allowlists and safe path constraints. |
|
||||
| Firewall/security | Firewall | engine-security | Security engine returns audit evidence artifacts. |
|
||||
| Backup and restore | Backups | engine-backup | Backup plans defined in Laravel. |
|
||||
| Agent session routing | Agents | agent-gateway | mTLS and command channel handling in gateway. |
|
||||
| Monitoring and alerts | Metrics, Alerts | metrics-ingest | Live streams + retained aggregates. |
|
||||
|
||||
## Cross-Context Contracts
|
||||
|
||||
- Commands are immutable envelopes with `idempotency_key`.
|
||||
- All control-plane writes emit domain events.
|
||||
- Engines are stateless workers and read policy-free command payloads.
|
||||
- Agents execute only capability-approved command types.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No direct shell command execution from Laravel workers.
|
||||
- No shared mutable state between engine services outside contract stores/queues.
|
||||
184
architecture/2026/02-core-schema.sql
Normal file
184
architecture/2026/02-core-schema.sql
Normal file
@@ -0,0 +1,184 @@
|
||||
-- YakPanel 2026 core schema
|
||||
-- PostgreSQL 15+ compatible
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
CREATE EXTENSION IF NOT EXISTS "citext";
|
||||
|
||||
-- Shared timestamps helper convention:
|
||||
-- created_at timestamptz not null default now()
|
||||
-- updated_at timestamptz not null default now()
|
||||
|
||||
CREATE TABLE tenants (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
name varchar(150) NOT NULL,
|
||||
slug citext NOT NULL UNIQUE,
|
||||
status varchar(32) NOT NULL DEFAULT 'active',
|
||||
plan_code varchar(64) NOT NULL DEFAULT 'starter',
|
||||
settings jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE users (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
email citext NOT NULL UNIQUE,
|
||||
password_hash text NOT NULL,
|
||||
display_name varchar(120) NOT NULL,
|
||||
is_platform_admin boolean NOT NULL DEFAULT false,
|
||||
last_login_at timestamptz NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE tenant_users (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
status varchar(32) NOT NULL DEFAULT 'active',
|
||||
joined_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (tenant_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE roles (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
name varchar(64) NOT NULL,
|
||||
description text NOT NULL DEFAULT '',
|
||||
is_system boolean NOT NULL DEFAULT false,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (tenant_id, name)
|
||||
);
|
||||
|
||||
CREATE TABLE permissions (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
code varchar(100) NOT NULL UNIQUE,
|
||||
description text NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE role_permissions (
|
||||
role_id uuid NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
permission_id uuid NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (role_id, permission_id)
|
||||
);
|
||||
|
||||
CREATE TABLE user_roles (
|
||||
tenant_user_id uuid NOT NULL REFERENCES tenant_users(id) ON DELETE CASCADE,
|
||||
role_id uuid NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (tenant_user_id, role_id)
|
||||
);
|
||||
|
||||
CREATE TABLE scopes (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
role_id uuid NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
resource_type varchar(64) NOT NULL,
|
||||
resource_id uuid NULL,
|
||||
action varchar(64) NOT NULL,
|
||||
effect varchar(8) NOT NULL DEFAULT 'allow',
|
||||
conditions jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE servers (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
name varchar(120) NOT NULL,
|
||||
host varchar(255) NOT NULL,
|
||||
port integer NOT NULL DEFAULT 443,
|
||||
os_type varchar(32) NOT NULL,
|
||||
status varchar(32) NOT NULL DEFAULT 'provisioning',
|
||||
region varchar(64) NOT NULL DEFAULT 'global',
|
||||
labels jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
heartbeat_at timestamptz NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (tenant_id, name)
|
||||
);
|
||||
|
||||
CREATE TABLE server_agents (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
server_id uuid NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
||||
agent_uid varchar(128) NOT NULL UNIQUE,
|
||||
agent_version varchar(32) NOT NULL,
|
||||
capabilities jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
cert_fingerprint varchar(255) NOT NULL,
|
||||
last_seen_at timestamptz NULL,
|
||||
status varchar(32) NOT NULL DEFAULT 'offline',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE jobs (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
server_id uuid NULL REFERENCES servers(id) ON DELETE SET NULL,
|
||||
requested_by_user_id uuid NULL REFERENCES users(id) ON DELETE SET NULL,
|
||||
type varchar(64) NOT NULL,
|
||||
status varchar(32) NOT NULL DEFAULT 'queued',
|
||||
priority smallint NOT NULL DEFAULT 5,
|
||||
idempotency_key varchar(120) NOT NULL,
|
||||
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
result jsonb NULL,
|
||||
started_at timestamptz NULL,
|
||||
finished_at timestamptz NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (tenant_id, idempotency_key)
|
||||
);
|
||||
|
||||
CREATE TABLE job_steps (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
job_id uuid NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
||||
step_order integer NOT NULL,
|
||||
name varchar(80) NOT NULL,
|
||||
status varchar(32) NOT NULL DEFAULT 'pending',
|
||||
input jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
output jsonb NULL,
|
||||
error text NULL,
|
||||
started_at timestamptz NULL,
|
||||
finished_at timestamptz NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (job_id, step_order)
|
||||
);
|
||||
|
||||
CREATE TABLE job_events (
|
||||
id bigserial PRIMARY KEY,
|
||||
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
job_id uuid NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
||||
event_type varchar(64) NOT NULL,
|
||||
source varchar(32) NOT NULL,
|
||||
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_job_events_tenant_job_created ON job_events (tenant_id, job_id, created_at DESC);
|
||||
|
||||
CREATE TABLE audit_logs (
|
||||
id bigserial PRIMARY KEY,
|
||||
tenant_id uuid NULL REFERENCES tenants(id) ON DELETE SET NULL,
|
||||
actor_user_id uuid NULL REFERENCES users(id) ON DELETE SET NULL,
|
||||
actor_type varchar(32) NOT NULL,
|
||||
action varchar(80) NOT NULL,
|
||||
resource_type varchar(64) NOT NULL,
|
||||
resource_id uuid NULL,
|
||||
request_id varchar(64) NULL,
|
||||
ip inet NULL,
|
||||
user_agent text NULL,
|
||||
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_logs_tenant_created ON audit_logs (tenant_id, created_at DESC);
|
||||
CREATE INDEX idx_scopes_tenant_action ON scopes (tenant_id, action);
|
||||
CREATE INDEX idx_servers_tenant_status ON servers (tenant_id, status);
|
||||
CREATE INDEX idx_jobs_tenant_status ON jobs (tenant_id, status);
|
||||
123
architecture/2026/03-agent-protocol-v1.md
Normal file
123
architecture/2026/03-agent-protocol-v1.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# YakPanel Agent Protocol v1
|
||||
|
||||
## Goals
|
||||
- Secure control channel between panel and managed servers.
|
||||
- Reliable command execution with resumable progress.
|
||||
- Capability-aware execution and strict auditability.
|
||||
|
||||
## Transport and Session
|
||||
- Primary transport: bidirectional gRPC stream over mTLS.
|
||||
- Fallback transport: WebSocket over mTLS.
|
||||
- Agent uses outbound connection only.
|
||||
- Server requires certificate pinning and protocol version negotiation.
|
||||
|
||||
## Version Negotiation
|
||||
- Agent sends `protocol_versions` during hello.
|
||||
- Gateway picks highest compatible version and responds with `selected_version`.
|
||||
- If no compatible version exists, gateway returns `ERR_UNSUPPORTED_VERSION`.
|
||||
|
||||
## Message Envelope (common fields)
|
||||
- `message_id` (uuid): unique per message.
|
||||
- `timestamp` (RFC3339 UTC): replay protection.
|
||||
- `tenant_id` (uuid): owning tenant.
|
||||
- `server_id` (uuid): managed server.
|
||||
- `agent_id` (string): persistent agent identity.
|
||||
- `trace_id` (string): distributed trace correlation.
|
||||
- `signature` (base64): detached signature over canonical payload.
|
||||
|
||||
## Enrollment Flow
|
||||
1. Agent starts with one-time bootstrap token.
|
||||
2. Agent calls `EnrollRequest` with host fingerprint and capabilities.
|
||||
3. Gateway validates token and issues short-lived enrollment certificate.
|
||||
4. Agent re-connects with cert; gateway persists `agent_id` and fingerprints.
|
||||
5. Enrollment token is revoked after successful bind.
|
||||
|
||||
## Heartbeat Flow
|
||||
- Interval: every 10 seconds.
|
||||
- Payload includes:
|
||||
- `agent_version`
|
||||
- `capability_hash`
|
||||
- `system_load` (cpu, mem, disk)
|
||||
- `service_states` (nginx/apache/ols/mysql/redis/docker)
|
||||
- Gateway updates presence and emits server status event.
|
||||
- Missing 3 consecutive heartbeats marks agent as degraded; 6 marks offline.
|
||||
|
||||
## Command Flow
|
||||
|
||||
### CommandRequest
|
||||
- `cmd_id` (uuid)
|
||||
- `job_id` (uuid)
|
||||
- `type` (enum): `SITE_CREATE`, `SITE_DELETE`, `SSL_ISSUE`, `SSL_APPLY`, `SERVICE_RELOAD`, `DOCKER_DEPLOY`, etc.
|
||||
- `args` (json map)
|
||||
- `timeout_sec` (int)
|
||||
- `idempotency_key` (string)
|
||||
- `required_capabilities` (string array)
|
||||
|
||||
### Agent behavior
|
||||
- Validate signature and timestamp window.
|
||||
- Validate required capability set.
|
||||
- Return ACK immediately with acceptance or reject reason.
|
||||
- Emit progress events at step boundaries.
|
||||
- Emit final result with exit code, summaries, and artifact references.
|
||||
|
||||
### Retry and Exactly-once strategy
|
||||
- Gateway retries only if no terminal result and timeout exceeded.
|
||||
- Agent deduplicates by `idempotency_key` and returns last known terminal state for duplicates.
|
||||
- Workflow layer (Laravel) provides exactly-once user-visible semantics.
|
||||
|
||||
## Event Types
|
||||
- `AGENT_HELLO`
|
||||
- `AGENT_HEARTBEAT`
|
||||
- `COMMAND_ACK`
|
||||
- `COMMAND_PROGRESS`
|
||||
- `COMMAND_LOG`
|
||||
- `COMMAND_RESULT`
|
||||
- `COMMAND_ERROR`
|
||||
- `AGENT_STATE_CHANGED`
|
||||
|
||||
## Error Model
|
||||
- Error codes:
|
||||
- `ERR_UNAUTHORIZED`
|
||||
- `ERR_UNSUPPORTED_VERSION`
|
||||
- `ERR_INVALID_SIGNATURE`
|
||||
- `ERR_REPLAY_DETECTED`
|
||||
- `ERR_CAPABILITY_MISSING`
|
||||
- `ERR_INVALID_ARGS`
|
||||
- `ERR_EXECUTION_FAILED`
|
||||
- `ERR_TIMEOUT`
|
||||
- Every error includes `code`, `message`, `retryable`, `details`.
|
||||
|
||||
## Security Controls
|
||||
- mTLS with rotating short-lived certs.
|
||||
- Nonce + timestamp replay protection.
|
||||
- Detached command signatures from panel signing key.
|
||||
- Agent command allowlist by capability.
|
||||
- Immutable append-only event log in control plane.
|
||||
|
||||
## Minimal Protobuf Sketch
|
||||
|
||||
```proto
|
||||
syntax = "proto3";
|
||||
package yakpanel.agent.v1;
|
||||
|
||||
message Envelope {
|
||||
string message_id = 1;
|
||||
string timestamp = 2;
|
||||
string tenant_id = 3;
|
||||
string server_id = 4;
|
||||
string agent_id = 5;
|
||||
string trace_id = 6;
|
||||
bytes signature = 7;
|
||||
}
|
||||
|
||||
message CommandRequest {
|
||||
Envelope envelope = 1;
|
||||
string cmd_id = 2;
|
||||
string job_id = 3;
|
||||
string type = 4;
|
||||
string args_json = 5;
|
||||
int32 timeout_sec = 6;
|
||||
string idempotency_key = 7;
|
||||
repeated string required_capabilities = 8;
|
||||
}
|
||||
```
|
||||
22
architecture/2026/04-server-plane-implementation.md
Normal file
22
architecture/2026/04-server-plane-implementation.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Server Plane Implementation (v1)
|
||||
|
||||
## Implemented Components
|
||||
- Laravel server registry routes and controllers under `panel-api`.
|
||||
- Agent enrollment/capability/session endpoints in `panel-api`.
|
||||
- Go command contract and dispatcher interface in `control-plane-go`.
|
||||
- `agent-gateway` process entrypoint for session and heartbeat responsibilities.
|
||||
|
||||
## End-to-End Control Flow
|
||||
1. User creates server via `POST /api/v1/servers`.
|
||||
2. User requests one-time enrollment token.
|
||||
3. Agent enrolls with gateway, obtains persistent identity.
|
||||
4. Laravel writes job and dispatches command envelope.
|
||||
5. Go dispatcher publishes command to execution topic.
|
||||
6. Agent executes command and streams results back.
|
||||
7. Laravel updates job state and exposes real-time status to UI.
|
||||
|
||||
## Next Implementation Edges
|
||||
- Persist server/agent entities in Eloquent models.
|
||||
- Add request validation and RBAC middleware to server routes.
|
||||
- Implement Redis Streams/NATS queue adapter for dispatcher queue interface.
|
||||
- Add gateway gRPC service and heartbeat/session persistence adapter.
|
||||
26
architecture/2026/05-hosting-mvp.md
Normal file
26
architecture/2026/05-hosting-mvp.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Hosting MVP Implementation Notes
|
||||
|
||||
## Scope
|
||||
- Website create/update/delete workflows.
|
||||
- Domain binding/unbinding.
|
||||
- SSL issue/apply/renew automation.
|
||||
- Webserver adapter support: Nginx, Apache, OpenLiteSpeed.
|
||||
|
||||
## Control Plane APIs
|
||||
- `GET /api/v1/sites`
|
||||
- `POST /api/v1/sites`
|
||||
- `POST /api/v1/sites/{site}/domains`
|
||||
- `POST /api/v1/ssl/issue`
|
||||
- `POST /api/v1/ssl/apply`
|
||||
- `POST /api/v1/ssl/renew`
|
||||
|
||||
## Execution Contracts
|
||||
- `SITE_CREATE`, `SITE_UPDATE`, `SITE_DELETE`
|
||||
- `DOMAIN_ADD`, `DOMAIN_REMOVE`
|
||||
- `SSL_ISSUE`, `SSL_APPLY`, `SSL_RENEW`
|
||||
- `WEBSERVER_RELOAD`
|
||||
|
||||
## Runtime Guarantees
|
||||
- Every operation is a tracked job with progress updates.
|
||||
- SSL operations require DNS precheck and challenge validation before apply.
|
||||
- Webserver reload only occurs after adapter-specific config validation.
|
||||
115
architecture/2026/06-api-structure.yaml
Normal file
115
architecture/2026/06-api-structure.yaml
Normal file
@@ -0,0 +1,115 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: YakPanel Control Plane API
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: /api/v1
|
||||
paths:
|
||||
/auth/login:
|
||||
post:
|
||||
tags: [Auth]
|
||||
summary: Authenticate user and return token
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
/tenants:
|
||||
get:
|
||||
tags: [Tenants]
|
||||
summary: List tenant memberships
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
/servers:
|
||||
get:
|
||||
tags: [Servers]
|
||||
summary: List managed servers
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
post:
|
||||
tags: [Servers]
|
||||
summary: Register server metadata
|
||||
responses:
|
||||
"202": { description: Accepted }
|
||||
/sites:
|
||||
get:
|
||||
tags: [Sites]
|
||||
summary: List sites
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
post:
|
||||
tags: [Sites]
|
||||
summary: Create site
|
||||
responses:
|
||||
"202": { description: Accepted }
|
||||
/ssl/issue:
|
||||
post:
|
||||
tags: [SSL]
|
||||
summary: Issue certificate
|
||||
responses:
|
||||
"202": { description: Accepted }
|
||||
/files/list:
|
||||
post:
|
||||
tags: [Files]
|
||||
summary: List files in allowed path scope
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
/cron/jobs:
|
||||
get:
|
||||
tags: [Cron]
|
||||
summary: List cron jobs
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
post:
|
||||
tags: [Cron]
|
||||
summary: Create cron job
|
||||
responses:
|
||||
"202": { description: Accepted }
|
||||
/firewall/rules:
|
||||
get:
|
||||
tags: [Firewall]
|
||||
summary: List firewall rules
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
post:
|
||||
tags: [Firewall]
|
||||
summary: Add firewall rule
|
||||
responses:
|
||||
"202": { description: Accepted }
|
||||
/backups/policies:
|
||||
get:
|
||||
tags: [Backups]
|
||||
summary: List backup policies
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
post:
|
||||
tags: [Backups]
|
||||
summary: Create backup policy
|
||||
responses:
|
||||
"201": { description: Created }
|
||||
/plugins/market:
|
||||
get:
|
||||
tags: [Marketplace]
|
||||
summary: Browse plugin marketplace
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
/plugins/install:
|
||||
post:
|
||||
tags: [Marketplace]
|
||||
summary: Install plugin
|
||||
responses:
|
||||
"202": { description: Accepted }
|
||||
/metrics/servers/{serverId}/live:
|
||||
get:
|
||||
tags: [Monitoring]
|
||||
summary: Get latest real-time server metrics
|
||||
parameters:
|
||||
- in: path
|
||||
name: serverId
|
||||
required: true
|
||||
schema: { type: string, format: uuid }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
37
architecture/2026/07-microservices-breakdown.md
Normal file
37
architecture/2026/07-microservices-breakdown.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Go Microservices Breakdown
|
||||
|
||||
## Services
|
||||
|
||||
### agent-gateway
|
||||
- Terminates mTLS channels from agents.
|
||||
- Tracks sessions, heartbeats, and capability maps.
|
||||
- Relays command envelopes and result events.
|
||||
|
||||
### engine-site
|
||||
- Manages site config generation, domain bindings, and SSL apply pipelines.
|
||||
- Supports Nginx, Apache, OpenLiteSpeed provider adapters.
|
||||
|
||||
### engine-docker
|
||||
- Deploys one-click apps via compose templates.
|
||||
- Handles image pulls, health checks, and rollback.
|
||||
|
||||
### engine-db
|
||||
- Manages MySQL and Redis lifecycle commands.
|
||||
- Executes create database/user, backup, restore primitives.
|
||||
|
||||
### engine-security
|
||||
- Applies firewall policies and security hardening commands.
|
||||
- Integrates fail2ban and baseline checks.
|
||||
|
||||
### engine-backup
|
||||
- Runs backup plans and restore workflows.
|
||||
- Pushes artifacts to object storage providers.
|
||||
|
||||
### engine-fileops
|
||||
- Secure file manager operations with path policy controls.
|
||||
- Chunked upload, download token issuance, lock-aware editing.
|
||||
|
||||
## Communication Model
|
||||
- Inbound command envelopes from Laravel orchestrator.
|
||||
- Internal queue topic fan-out by command type.
|
||||
- Outbound event stream for progress, logs, and terminal results.
|
||||
58
architecture/2026/08-folder-structure.md
Normal file
58
architecture/2026/08-folder-structure.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Suggested 2026 Folder Structure
|
||||
|
||||
## Root Layout
|
||||
|
||||
```text
|
||||
YakPanel-master/
|
||||
panel-api/ # Laravel control plane
|
||||
panel-web/ # Next.js + Tailwind UI
|
||||
control-plane-go/ # Go execution services
|
||||
yak-agent/ # Go daemon on managed servers
|
||||
architecture/2026/ # Blueprint and contracts
|
||||
```
|
||||
|
||||
## Laravel (`panel-api`)
|
||||
- `app/Modules/Auth`
|
||||
- `app/Modules/Tenant`
|
||||
- `app/Modules/Rbac`
|
||||
- `app/Modules/Server`
|
||||
- `app/Modules/Agents`
|
||||
- `app/Modules/Site`
|
||||
- `app/Modules/Ssl`
|
||||
- `app/Modules/Files`
|
||||
- `app/Modules/Cron`
|
||||
- `app/Modules/Firewall`
|
||||
- `app/Modules/Backups`
|
||||
- `app/Modules/Plugin`
|
||||
- `app/Modules/Metrics`
|
||||
- `routes/api_v1`
|
||||
|
||||
## Next.js (`panel-web`)
|
||||
- `src/app/(dashboard)`
|
||||
- `src/features/server`
|
||||
- `src/features/sites`
|
||||
- `src/features/marketplace`
|
||||
- `src/features/metrics`
|
||||
- `src/lib/api`
|
||||
- `src/lib/ws`
|
||||
|
||||
## Go control-plane (`control-plane-go`)
|
||||
- `cmd/agent-gateway`
|
||||
- `cmd/engine-site`
|
||||
- `cmd/engine-docker`
|
||||
- `cmd/engine-db`
|
||||
- `cmd/engine-security`
|
||||
- `cmd/engine-backup`
|
||||
- `cmd/engine-fileops`
|
||||
- `internal/orchestration`
|
||||
- `internal/webserver`
|
||||
- `pkg/contracts`
|
||||
- `pkg/proto`
|
||||
|
||||
## Go agent (`yak-agent`)
|
||||
- `cmd/agent`
|
||||
- `internal/transport`
|
||||
- `internal/executor`
|
||||
- `internal/collectors`
|
||||
- `internal/updater`
|
||||
- `pkg/capabilities`
|
||||
30
architecture/2026/09-development-roadmap.md
Normal file
30
architecture/2026/09-development-roadmap.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# YakPanel 2026 Development Roadmap
|
||||
|
||||
## Phase 1: Foundation (Weeks 1-3)
|
||||
- Stand up repositories and CI pipelines.
|
||||
- Define coding standards, security checks, and observability baseline.
|
||||
- Establish Docker-based developer environment.
|
||||
|
||||
## Phase 2: Identity + Tenancy (Weeks 4-6)
|
||||
- Implement tenants, users, tenant membership, and RBAC scopes.
|
||||
- Add API tokens, session controls, and audit logging.
|
||||
|
||||
## Phase 3: Server Plane (Weeks 7-10)
|
||||
- Implement server registry, agent enrollment tokens, and live sessions.
|
||||
- Deliver command orchestration and event stream updates.
|
||||
|
||||
## Phase 4: Hosting MVP (Weeks 11-15)
|
||||
- Implement sites/domains/SSL workflows.
|
||||
- Add Nginx/Apache/OpenLiteSpeed adapters and validation gates.
|
||||
|
||||
## Phase 5: Platform Operations (Weeks 16-20)
|
||||
- Deliver file manager, cron manager, firewall, backup policy engine.
|
||||
- Add MySQL/Redis lifecycle management.
|
||||
|
||||
## Phase 6: Marketplace + Monitoring (Weeks 21-25)
|
||||
- Deliver plugin marketplace catalog and signed install pipeline.
|
||||
- Build real-time metrics dashboard and alerting engine.
|
||||
|
||||
## Phase 7: SaaS Hardening (Weeks 26-30)
|
||||
- Quotas, plan enforcement, webhook integrations, public API.
|
||||
- Multi-region readiness and disaster recovery rehearsals.
|
||||
41
architecture/2026/10-ops-marketplace-observability.md
Normal file
41
architecture/2026/10-ops-marketplace-observability.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Ops, Marketplace, and Observability Implementation
|
||||
|
||||
## Ops Modules
|
||||
|
||||
### File Manager
|
||||
- Command types: `FILE_LIST`, `FILE_READ`, `FILE_WRITE`, `FILE_UPLOAD_CHUNK`, `FILE_MOVE`, `FILE_DELETE`.
|
||||
- Security: path sandbox, denylist for system paths, content size and MIME checks.
|
||||
|
||||
### Cron Manager
|
||||
- Command types: `CRON_LIST`, `CRON_CREATE`, `CRON_UPDATE`, `CRON_DELETE`.
|
||||
- Validation: cron expression parser, command allowlist/templating, dry-run syntax checks.
|
||||
|
||||
### Firewall + Security Tools
|
||||
- Command types: `FIREWALL_RULE_ADD`, `FIREWALL_RULE_DELETE`, `SECURITY_SCAN_BASELINE`.
|
||||
- Audit: every mutation recorded with actor, reason, and approval trace.
|
||||
|
||||
### Backup & Restore
|
||||
- Command types: `BACKUP_RUN`, `BACKUP_RESTORE`, `BACKUP_VERIFY`.
|
||||
- Flows: policy-driven schedules, retention lifecycle, encrypted object storage artifacts.
|
||||
|
||||
## Plugin Marketplace
|
||||
- Catalog includes signed package metadata, compatibility matrix, and permission manifest.
|
||||
- Install pipeline:
|
||||
1. Resolve package and verify signature.
|
||||
2. Validate required capabilities and tenant entitlement.
|
||||
3. Execute install as job with rollback hooks.
|
||||
4. Persist install status and event timeline.
|
||||
|
||||
## Real-time Monitoring Dashboard
|
||||
- Live channels: CPU, RAM, disk I/O, network throughput, process health.
|
||||
- Pipeline:
|
||||
- agent collectors -> gateway ingest -> Redis stream -> metrics store.
|
||||
- UI:
|
||||
- server list health badges,
|
||||
- per-server timeline charts,
|
||||
- alert panels with acknowledgement workflow.
|
||||
|
||||
## Performance Targets
|
||||
- P95 live metric latency: < 2 seconds.
|
||||
- P95 command dispatch to agent ACK: < 1 second.
|
||||
- P95 dashboard query time (last 1 hour): < 400 ms.
|
||||
41
architecture/2026/11-identity-core-schema.sql
Normal file
41
architecture/2026/11-identity-core-schema.sql
Normal file
@@ -0,0 +1,41 @@
|
||||
-- YakPanel 2026 Identity Core additions
|
||||
|
||||
CREATE TABLE IF NOT EXISTS api_tokens (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name varchar(80) NOT NULL,
|
||||
token_hash varchar(255) NOT NULL UNIQUE,
|
||||
scopes jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
expires_at timestamptz NULL,
|
||||
last_used_at timestamptz NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
refresh_token_hash varchar(255) NOT NULL UNIQUE,
|
||||
ip inet NULL,
|
||||
user_agent text NULL,
|
||||
expires_at timestamptz NOT NULL,
|
||||
revoked_at timestamptz NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mfa_factors (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
factor_type varchar(24) NOT NULL,
|
||||
secret_encrypted text NOT NULL,
|
||||
recovery_codes_encrypted text NULL,
|
||||
verified_at timestamptz NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_api_tokens_tenant_user ON api_tokens (tenant_id, user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_tenant_user ON sessions (tenant_id, user_id);
|
||||
24
architecture/2026/12-identity-phase-implementation.md
Normal file
24
architecture/2026/12-identity-phase-implementation.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Phase 2 Implementation: Identity Core
|
||||
|
||||
## Delivered
|
||||
- Identity route group in `panel-api/routes/api_v1/identity.php`.
|
||||
- Controllers for auth, tenants, and RBAC module boundaries:
|
||||
- `AuthController`
|
||||
- `TenantController`
|
||||
- `RbacController`
|
||||
- Scope evaluator service for allow/deny decision logic:
|
||||
- `ScopeEvaluator::isAllowed(...)`
|
||||
- Identity schema additions:
|
||||
- `api_tokens`
|
||||
- `sessions`
|
||||
- `mfa_factors`
|
||||
|
||||
## Behavior Contract
|
||||
- Every protected endpoint requires bearer auth middleware.
|
||||
- Access checks are explicit through RBAC grant evaluation.
|
||||
- Session/token tables support rotation, revocation, and forensic tracking.
|
||||
|
||||
## Next phase options
|
||||
- Wire persistent Eloquent models and form requests.
|
||||
- Add tenant-aware middleware that injects active tenant context.
|
||||
- Replace placeholder auth responses with JWT + refresh token issue/rotation.
|
||||
25
architecture/2026/13-server-plane-phase-implementation.md
Normal file
25
architecture/2026/13-server-plane-phase-implementation.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Phase 3 Implementation: Server Plane
|
||||
|
||||
## Delivered in this phase
|
||||
- Tenant-aware server registry controller behavior via request tenant context.
|
||||
- Agent session heartbeat intake endpoint and session repository boundary.
|
||||
- Job dispatch endpoint with command type allowlist and idempotent envelope fields.
|
||||
- Redis Streams queue adapter for Go orchestration layer.
|
||||
|
||||
## Files added/updated
|
||||
- `panel-api/app/Modules/Server/ServerRepository.php`
|
||||
- `panel-api/app/Modules/Agents/AgentSessionRepository.php`
|
||||
- `panel-api/app/Modules/Jobs/JobController.php`
|
||||
- `panel-api/app/Modules/Jobs/CommandOrchestrator.php`
|
||||
- `panel-api/app/Http/Middleware/ResolveTenantContext.php`
|
||||
- `panel-api/routes/api_v1/servers.php`
|
||||
- `panel-api/routes/api_v1/jobs.php`
|
||||
- `control-plane-go/internal/orchestration/redis_stream_queue.go`
|
||||
|
||||
## API additions
|
||||
- `POST /api/v1/servers/{server}/heartbeat`
|
||||
- `POST /api/v1/jobs/dispatch`
|
||||
|
||||
## Notes
|
||||
- Persistence adapters remain intentionally thin to keep boundary clear for full Eloquent integration.
|
||||
- Queue adapter is production-aligned with Redis Streams and can be swapped with NATS without controller changes.
|
||||
30
architecture/2026/14-server-plane-schema-additions.sql
Normal file
30
architecture/2026/14-server-plane-schema-additions.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- Optional server-plane extension tables
|
||||
|
||||
CREATE TABLE IF NOT EXISTS command_dispatches (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
job_id uuid NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
||||
server_id uuid NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
||||
command_type varchar(64) NOT NULL,
|
||||
idempotency_key varchar(120) NOT NULL,
|
||||
status varchar(32) NOT NULL DEFAULT 'queued',
|
||||
queued_at timestamptz NOT NULL DEFAULT now(),
|
||||
dispatched_at timestamptz NULL,
|
||||
acked_at timestamptz NULL,
|
||||
finished_at timestamptz NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (tenant_id, idempotency_key)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_heartbeats (
|
||||
id bigserial PRIMARY KEY,
|
||||
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
server_id uuid NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
||||
agent_uid varchar(128) NOT NULL,
|
||||
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_heartbeats_server_created
|
||||
ON agent_heartbeats (server_id, created_at DESC);
|
||||
23
architecture/2026/15-hosting-mvp-runtime-wiring.md
Normal file
23
architecture/2026/15-hosting-mvp-runtime-wiring.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Phase 4 Implementation: Hosting MVP Runtime Wiring
|
||||
|
||||
## Delivered
|
||||
- Site/domain/SSL controllers now delegate to a single workflow service.
|
||||
- Hosting command envelopes are generated through `HostingCommandFactory`.
|
||||
- Webserver profile normalization supports:
|
||||
- `nginx`
|
||||
- `apache` (`httpd` alias)
|
||||
- `openlitespeed` (`ols` alias)
|
||||
- Command orchestrator now accepts:
|
||||
- `DOMAIN_ADD`
|
||||
- `DOMAIN_REMOVE`
|
||||
- `WEBSERVER_RELOAD`
|
||||
|
||||
## Runtime flow
|
||||
1. API receives request for site/domain/ssl mutation.
|
||||
2. `SiteWorkflowService` validates and maps request to typed command envelope.
|
||||
3. `CommandOrchestrator` assigns job metadata and enqueues dispatch payload.
|
||||
4. Execution plane consumes command and performs adapter-specific operation.
|
||||
|
||||
## Notes
|
||||
- This phase intentionally keeps persistence minimal while making runtime wiring explicit.
|
||||
- Next step is adding provider-specific validation templates and dry-run checks before dispatch.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Phase 5 Implementation: Platform Operations
|
||||
|
||||
## Delivered Modules
|
||||
|
||||
### File Manager
|
||||
- API:
|
||||
- `POST /api/v1/files/list`
|
||||
- `POST /api/v1/files/read`
|
||||
- `POST /api/v1/files/write`
|
||||
- Command types:
|
||||
- `FILE_LIST`, `FILE_READ`, `FILE_WRITE`
|
||||
|
||||
### Cron Manager
|
||||
- API:
|
||||
- `GET /api/v1/cron/jobs`
|
||||
- `POST /api/v1/cron/jobs`
|
||||
- `DELETE /api/v1/cron/jobs`
|
||||
- Command types:
|
||||
- `CRON_LIST`, `CRON_CREATE`, `CRON_DELETE`
|
||||
|
||||
### Firewall
|
||||
- API:
|
||||
- `GET /api/v1/firewall/rules`
|
||||
- `POST /api/v1/firewall/rules`
|
||||
- `DELETE /api/v1/firewall/rules`
|
||||
- Command types:
|
||||
- `FIREWALL_RULE_LIST`, `FIREWALL_RULE_ADD`, `FIREWALL_RULE_DELETE`
|
||||
|
||||
### Backup and Restore
|
||||
- API:
|
||||
- `POST /api/v1/backups/run`
|
||||
- `POST /api/v1/backups/restore`
|
||||
- Command types:
|
||||
- `BACKUP_RUN`, `BACKUP_RESTORE`
|
||||
|
||||
## Implementation Notes
|
||||
- `OpsCommandFactory` centralizes operation command envelope construction.
|
||||
- `OpsWorkflowService` ensures all operations route through `CommandOrchestrator`.
|
||||
- Operation endpoints are now tenant-context aware and dispatch-ready.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Phase 6 Implementation: Marketplace and Monitoring
|
||||
|
||||
## Delivered Marketplace scope
|
||||
- Added marketplace workflow and command factory:
|
||||
- `MarketplaceWorkflowService`
|
||||
- `MarketplaceCommandFactory`
|
||||
- Added marketplace controller:
|
||||
- Catalog browsing endpoint
|
||||
- Plugin install/update/remove endpoints
|
||||
- Added routes:
|
||||
- `GET /api/v1/plugin-market`
|
||||
- `POST /api/v1/plugins/install`
|
||||
- `POST /api/v1/plugins/update`
|
||||
- `POST /api/v1/plugins/remove`
|
||||
- Added command types:
|
||||
- `PLUGIN_INSTALL`
|
||||
- `PLUGIN_UPDATE`
|
||||
- `PLUGIN_REMOVE`
|
||||
|
||||
## Delivered Monitoring scope
|
||||
- Added monitoring controller:
|
||||
- `GET /api/v1/metrics/servers/{serverId}/live`
|
||||
- `POST /api/v1/alerts/rules`
|
||||
- `GET /api/v1/alerts`
|
||||
- Added ingest controller:
|
||||
- `POST /api/v1/metrics/ingest`
|
||||
|
||||
## Architecture notes
|
||||
- Marketplace lifecycle operations are routed through the same command orchestration contract as other platform operations.
|
||||
- Monitoring includes both pull-oriented live endpoint patterns and push-oriented ingest patterns for agent collectors.
|
||||
60
architecture/2026/18-saas-hardening-schema.sql
Normal file
60
architecture/2026/18-saas-hardening-schema.sql
Normal file
@@ -0,0 +1,60 @@
|
||||
-- SaaS hardening schema additions
|
||||
|
||||
CREATE TABLE IF NOT EXISTS plan_limits (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
plan_code varchar(64) NOT NULL,
|
||||
resource varchar(64) NOT NULL,
|
||||
hard_limit integer NOT NULL,
|
||||
soft_limit integer NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (plan_code, resource)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS usage_counters (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
resource varchar(64) NOT NULL,
|
||||
usage_value bigint NOT NULL DEFAULT 0,
|
||||
period_start timestamptz NOT NULL,
|
||||
period_end timestamptz NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (tenant_id, resource, period_start, period_end)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webhook_endpoints (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
url text NOT NULL,
|
||||
secret_encrypted text NOT NULL,
|
||||
events jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
status varchar(32) NOT NULL DEFAULT 'active',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webhook_deliveries (
|
||||
id bigserial PRIMARY KEY,
|
||||
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
webhook_endpoint_id uuid NOT NULL REFERENCES webhook_endpoints(id) ON DELETE CASCADE,
|
||||
event_type varchar(64) NOT NULL,
|
||||
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
status varchar(32) NOT NULL DEFAULT 'queued',
|
||||
response_code integer NULL,
|
||||
attempts integer NOT NULL DEFAULT 0,
|
||||
delivered_at timestamptz NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public_api_tokens (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
name varchar(120) NOT NULL,
|
||||
token_hash varchar(255) NOT NULL UNIQUE,
|
||||
scopes jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
expires_at timestamptz NULL,
|
||||
revoked_at timestamptz NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
34
architecture/2026/19-saas-hardening-phase-implementation.md
Normal file
34
architecture/2026/19-saas-hardening-phase-implementation.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Phase 7 Implementation: SaaS Hardening
|
||||
|
||||
## Delivered scope
|
||||
|
||||
### Quotas and plan enforcement hooks
|
||||
- Added `QuotaService` for tenant resource checks.
|
||||
- Added `BillingHooksService` for usage event emission.
|
||||
- Added APIs:
|
||||
- `POST /api/v1/saas/quotas/check`
|
||||
- `POST /api/v1/saas/usage-events`
|
||||
|
||||
### Webhook management
|
||||
- Added webhook controller for endpoint CRUD/test placeholders.
|
||||
- Added APIs:
|
||||
- `GET /api/v1/webhooks`
|
||||
- `POST /api/v1/webhooks`
|
||||
- `POST /api/v1/webhooks/test`
|
||||
|
||||
### Scoped public API
|
||||
- Added token service and public API controller.
|
||||
- Added APIs:
|
||||
- `POST /api/v1/public/tokens`
|
||||
- `POST /api/v1/public/tokens/introspect`
|
||||
|
||||
## Data model additions
|
||||
- `plan_limits`
|
||||
- `usage_counters`
|
||||
- `webhook_endpoints`
|
||||
- `webhook_deliveries`
|
||||
- `public_api_tokens`
|
||||
|
||||
## Notes
|
||||
- This phase establishes hardening boundaries and contracts.
|
||||
- Token/hash generation, secret encryption, and retry workers are the next productionization tasks.
|
||||
8
control-plane-go/cmd/agent-gateway/main.go
Normal file
8
control-plane-go/cmd/agent-gateway/main.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package main
|
||||
|
||||
import "log"
|
||||
|
||||
func main() {
|
||||
log.Println("yakpanel agent-gateway bootstrap")
|
||||
log.Println("responsibilities: mTLS sessions, heartbeat intake, command channel routing")
|
||||
}
|
||||
5
control-plane-go/go.mod
Normal file
5
control-plane-go/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module yakpanel/control-plane-go
|
||||
|
||||
go 1.23
|
||||
|
||||
require github.com/redis/go-redis/v9 v9.6.1
|
||||
19
control-plane-go/internal/orchestration/dispatcher.go
Normal file
19
control-plane-go/internal/orchestration/dispatcher.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package orchestration
|
||||
|
||||
import "yakpanel/control-plane-go/pkg/contracts"
|
||||
|
||||
type Queue interface {
|
||||
Publish(topic string, payload any) error
|
||||
}
|
||||
|
||||
type Dispatcher struct {
|
||||
queue Queue
|
||||
}
|
||||
|
||||
func NewDispatcher(queue Queue) *Dispatcher {
|
||||
return &Dispatcher{queue: queue}
|
||||
}
|
||||
|
||||
func (d *Dispatcher) DispatchCommand(cmd contracts.CommandEnvelope) error {
|
||||
return d.queue.Publish("yakpanel.commands", cmd)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package orchestration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type RedisStreamQueue struct {
|
||||
client *redis.Client
|
||||
stream string
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewRedisStreamQueue(ctx context.Context, client *redis.Client, stream string) *RedisStreamQueue {
|
||||
return &RedisStreamQueue{
|
||||
client: client,
|
||||
stream: stream,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
func (q *RedisStreamQueue) Publish(topic string, payload any) error {
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = q.client.XAdd(q.ctx, &redis.XAddArgs{
|
||||
Stream: q.stream,
|
||||
Values: map[string]any{
|
||||
"topic": topic,
|
||||
"body": string(body),
|
||||
},
|
||||
}).Result()
|
||||
return err
|
||||
}
|
||||
42
control-plane-go/internal/webserver/adapters.go
Normal file
42
control-plane-go/internal/webserver/adapters.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package webserver
|
||||
|
||||
import "errors"
|
||||
|
||||
type Adapter interface {
|
||||
Name() string
|
||||
ValidateVHost(config string) error
|
||||
Reload() error
|
||||
}
|
||||
|
||||
type NginxAdapter struct{}
|
||||
type ApacheAdapter struct{}
|
||||
type OpenLiteSpeedAdapter struct{}
|
||||
|
||||
func (a NginxAdapter) Name() string { return "nginx" }
|
||||
func (a ApacheAdapter) Name() string { return "apache" }
|
||||
func (a OpenLiteSpeedAdapter) Name() string { return "openlitespeed" }
|
||||
|
||||
func (a NginxAdapter) ValidateVHost(config string) error {
|
||||
if config == "" {
|
||||
return errors.New("empty nginx vhost config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a ApacheAdapter) ValidateVHost(config string) error {
|
||||
if config == "" {
|
||||
return errors.New("empty apache vhost config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a OpenLiteSpeedAdapter) ValidateVHost(config string) error {
|
||||
if config == "" {
|
||||
return errors.New("empty openlitespeed vhost config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a NginxAdapter) Reload() error { return nil }
|
||||
func (a ApacheAdapter) Reload() error { return nil }
|
||||
func (a OpenLiteSpeedAdapter) Reload() error { return nil }
|
||||
22
control-plane-go/pkg/contracts/command.go
Normal file
22
control-plane-go/pkg/contracts/command.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package contracts
|
||||
|
||||
type CommandEnvelope struct {
|
||||
CommandID string `json:"cmd_id"`
|
||||
JobID string `json:"job_id"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
ServerID string `json:"server_id"`
|
||||
Type string `json:"type"`
|
||||
Args map[string]any `json:"args"`
|
||||
TimeoutSec int `json:"timeout_sec"`
|
||||
IdempotencyKey string `json:"idempotency_key"`
|
||||
RequiredCaps []string `json:"required_capabilities"`
|
||||
RequestedByUserID string `json:"requested_by_user_id"`
|
||||
}
|
||||
|
||||
type CommandResult struct {
|
||||
CommandID string `json:"cmd_id"`
|
||||
Status string `json:"status"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
Output map[string]any `json:"output"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
112
docker-compose.yakpanel.yml
Normal file
112
docker-compose.yakpanel.yml
Normal file
@@ -0,0 +1,112 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: yakpanel-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
ports:
|
||||
- "${POSTGRES_PORT}:5432"
|
||||
volumes:
|
||||
- yakpanel_postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: yakpanel-redis
|
||||
restart: unless-stopped
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
ports:
|
||||
- "${REDIS_PORT}:6379"
|
||||
volumes:
|
||||
- yakpanel_redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
nats:
|
||||
image: nats:2.10-alpine
|
||||
container_name: yakpanel-nats
|
||||
restart: unless-stopped
|
||||
command: ["-js"]
|
||||
ports:
|
||||
- "${NATS_PORT}:4222"
|
||||
- "${NATS_MONITOR_PORT}:8222"
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: yakpanel-minio
|
||||
restart: unless-stopped
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||
ports:
|
||||
- "${MINIO_PORT}:9000"
|
||||
- "${MINIO_CONSOLE_PORT}:9001"
|
||||
volumes:
|
||||
- yakpanel_minio_data:/data
|
||||
|
||||
api:
|
||||
image: php:8.3-cli-alpine
|
||||
container_name: yakpanel-api
|
||||
restart: unless-stopped
|
||||
working_dir: /var/www/panel-api
|
||||
command: ["php", "-S", "0.0.0.0:8080", "-t", "public"]
|
||||
volumes:
|
||||
- ./panel-api:/var/www/panel-api
|
||||
ports:
|
||||
- "${API_PORT}:8080"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
db-migrate:
|
||||
condition: service_completed_successfully
|
||||
|
||||
agent-gateway:
|
||||
image: golang:1.23-alpine
|
||||
container_name: yakpanel-agent-gateway
|
||||
restart: unless-stopped
|
||||
working_dir: /src
|
||||
command: ["sh", "-lc", "go run ./cmd/agent-gateway"]
|
||||
volumes:
|
||||
- ./control-plane-go:/src
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
db-migrate:
|
||||
image: postgres:16-alpine
|
||||
container_name: yakpanel-db-migrate
|
||||
restart: "no"
|
||||
working_dir: /workspace
|
||||
environment:
|
||||
PGPASSWORD: ${POSTGRES_PASSWORD}
|
||||
command:
|
||||
[
|
||||
"sh",
|
||||
"-lc",
|
||||
"psql -h postgres -U ${POSTGRES_USER} -d ${POSTGRES_DB} -f architecture/2026/02-core-schema.sql && psql -h postgres -U ${POSTGRES_USER} -d ${POSTGRES_DB} -f architecture/2026/11-identity-core-schema.sql && psql -h postgres -U ${POSTGRES_USER} -d ${POSTGRES_DB} -f architecture/2026/14-server-plane-schema-additions.sql && psql -h postgres -U ${POSTGRES_USER} -d ${POSTGRES_DB} -f architecture/2026/18-saas-hardening-schema.sql"
|
||||
]
|
||||
volumes:
|
||||
- ./:/workspace
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
yakpanel_postgres_data:
|
||||
yakpanel_redis_data:
|
||||
yakpanel_minio_data:
|
||||
43
docs/ubuntu-dev-install.md
Normal file
43
docs/ubuntu-dev-install.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# YakPanel Ubuntu Dev Install
|
||||
|
||||
This setup brings up the YakPanel 2026 scaffold stack on Ubuntu 22.04/24.04.
|
||||
|
||||
## Quick install
|
||||
|
||||
```bash
|
||||
cd /path/to/YakPanel-master
|
||||
chmod +x scripts/install-ubuntu.sh scripts/bootstrap-dev.sh
|
||||
./scripts/install-ubuntu.sh
|
||||
```
|
||||
|
||||
## One-click install
|
||||
|
||||
```bash
|
||||
cd /path/to/YakPanel-master
|
||||
chmod +x one-click-installer.sh
|
||||
./one-click-installer.sh
|
||||
```
|
||||
|
||||
## Start/stop commands
|
||||
|
||||
```bash
|
||||
make init
|
||||
make up
|
||||
make migrate
|
||||
make ps
|
||||
make logs
|
||||
make down
|
||||
```
|
||||
|
||||
## Services
|
||||
- API health: `http://localhost:8080/health`
|
||||
- PostgreSQL: `localhost:5432`
|
||||
- Redis: `localhost:6379`
|
||||
- NATS monitor: `http://localhost:8222`
|
||||
- MinIO console: `http://localhost:9001`
|
||||
|
||||
## Notes
|
||||
- This is a development scaffold for the architecture implementation.
|
||||
- `panel-api` currently serves a minimal runtime entrypoint for health and bootstrap validation.
|
||||
- Full Laravel production bootstrap (artisan app, providers, migrations runtime wiring) is the next implementation step.
|
||||
- Database schema is automatically applied by `db-migrate` during bootstrap and can be re-run with `make migrate`.
|
||||
41
one-click-installer.sh
Normal file
41
one-click-installer.sh
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
require_ubuntu() {
|
||||
if [[ ! -f /etc/os-release ]]; then
|
||||
echo "Unsupported OS: missing /etc/os-release"
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
if [[ "${ID:-}" != "ubuntu" ]]; then
|
||||
echo "This one-click installer currently supports Ubuntu only."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_executable_scripts() {
|
||||
chmod +x "${PROJECT_DIR}/scripts/install-ubuntu.sh"
|
||||
chmod +x "${PROJECT_DIR}/scripts/bootstrap-dev.sh"
|
||||
chmod +x "${PROJECT_DIR}/scripts/migrate-dev.sh"
|
||||
}
|
||||
|
||||
main() {
|
||||
require_ubuntu
|
||||
ensure_executable_scripts
|
||||
|
||||
echo "YakPanel one-click installer started..."
|
||||
cd "${PROJECT_DIR}"
|
||||
|
||||
"${PROJECT_DIR}/scripts/install-ubuntu.sh"
|
||||
|
||||
echo
|
||||
echo "YakPanel installation completed."
|
||||
echo "API health: http://localhost:8080/health"
|
||||
echo "MinIO console: http://localhost:9001"
|
||||
echo "NATS monitor: http://localhost:8222"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
22
panel-api/README.md
Normal file
22
panel-api/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# YakPanel Control Plane API (Laravel)
|
||||
|
||||
This folder defines the 2026 Laravel control plane boundary.
|
||||
|
||||
## Purpose
|
||||
- API gateway for UI and third-party integrations.
|
||||
- Tenant/RBAC enforcement before any execution request.
|
||||
- Workflow orchestration and audit trail for system operations.
|
||||
|
||||
## Initial modules
|
||||
- `app/Modules/Server`: inventory and server lifecycle metadata.
|
||||
- `app/Modules/Agents`: enrollment tokens, agent sessions, heartbeat updates.
|
||||
- `app/Modules/Jobs`: command orchestration and execution state tracking.
|
||||
- `app/Modules/Auth`: login/refresh/me/logout boundaries.
|
||||
- `app/Modules/Tenant`: tenant lifecycle and membership boundaries.
|
||||
- `app/Modules/Rbac`: role/permission/scope checks.
|
||||
- `routes/api_v1`: versioned route groups.
|
||||
|
||||
## Integration contract
|
||||
- Never execute privileged shell operations directly in Laravel.
|
||||
- Dispatch immutable command envelopes to Go execution plane.
|
||||
- Persist all requests as jobs for visibility and replay-safe recovery.
|
||||
18
panel-api/app/Http/Middleware/ResolveTenantContext.php
Normal file
18
panel-api/app/Http/Middleware/ResolveTenantContext.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ResolveTenantContext
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$tenantId = (string) $request->header('X-Tenant-Id', '');
|
||||
$request->attributes->set('tenant_id', $tenantId);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
55
panel-api/app/Modules/Agents/AgentController.php
Normal file
55
panel-api/app/Modules/Agents/AgentController.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Agents;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class AgentController extends Controller
|
||||
{
|
||||
public function __construct(private readonly AgentSessionRepository $sessionsRepo)
|
||||
{
|
||||
}
|
||||
|
||||
public function issueEnrollmentToken(string $server): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'server_id' => $server,
|
||||
'token_type' => 'bootstrap',
|
||||
'expires_in_sec' => 900,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function capabilities(string $server): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'server_id' => $server,
|
||||
'capabilities' => [],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function sessions(string $server): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'server_id' => $server,
|
||||
'sessions' => $this->sessionsRepo->listByServer($server),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function heartbeat(Request $request, string $server): JsonResponse
|
||||
{
|
||||
$agentUid = (string) $request->input('agent_uid', '');
|
||||
$session = $this->sessionsRepo->touchHeartbeat($server, $agentUid, $request->all());
|
||||
|
||||
return response()->json([
|
||||
'data' => $session,
|
||||
]);
|
||||
}
|
||||
}
|
||||
22
panel-api/app/Modules/Agents/AgentSessionRepository.php
Normal file
22
panel-api/app/Modules/Agents/AgentSessionRepository.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Agents;
|
||||
|
||||
class AgentSessionRepository
|
||||
{
|
||||
public function listByServer(string $serverId): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function touchHeartbeat(string $serverId, string $agentUid, array $payload): array
|
||||
{
|
||||
return [
|
||||
'server_id' => $serverId,
|
||||
'agent_uid' => $agentUid,
|
||||
'status' => 'online',
|
||||
'last_seen_at' => now()->toISOString(),
|
||||
'payload' => $payload,
|
||||
];
|
||||
}
|
||||
}
|
||||
48
panel-api/app/Modules/Auth/AuthController.php
Normal file
48
panel-api/app/Modules/Auth/AuthController.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Auth;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'access_token' => 'placeholder-token',
|
||||
'token_type' => 'Bearer',
|
||||
'expires_in' => 3600,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function refresh(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'access_token' => 'placeholder-token-refreshed',
|
||||
'token_type' => 'Bearer',
|
||||
'expires_in' => 3600,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function me(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'id' => null,
|
||||
'email' => null,
|
||||
'tenants' => [],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([], 204);
|
||||
}
|
||||
}
|
||||
54
panel-api/app/Modules/Jobs/CommandOrchestrator.php
Normal file
54
panel-api/app/Modules/Jobs/CommandOrchestrator.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Jobs;
|
||||
|
||||
class CommandOrchestrator
|
||||
{
|
||||
private array $acceptedTypes = [
|
||||
'SITE_CREATE',
|
||||
'SITE_UPDATE',
|
||||
'SITE_DELETE',
|
||||
'DOMAIN_ADD',
|
||||
'DOMAIN_REMOVE',
|
||||
'SSL_ISSUE',
|
||||
'SSL_APPLY',
|
||||
'SSL_RENEW',
|
||||
'WEBSERVER_RELOAD',
|
||||
'FILE_LIST',
|
||||
'FILE_READ',
|
||||
'FILE_WRITE',
|
||||
'CRON_LIST',
|
||||
'CRON_CREATE',
|
||||
'CRON_DELETE',
|
||||
'FIREWALL_RULE_LIST',
|
||||
'DOCKER_DEPLOY',
|
||||
'PLUGIN_INSTALL',
|
||||
'PLUGIN_UPDATE',
|
||||
'PLUGIN_REMOVE',
|
||||
'FIREWALL_RULE_ADD',
|
||||
'FIREWALL_RULE_DELETE',
|
||||
'BACKUP_RUN',
|
||||
'BACKUP_RESTORE',
|
||||
];
|
||||
|
||||
public function dispatch(array $commandEnvelope): array
|
||||
{
|
||||
$type = (string) ($commandEnvelope['type'] ?? '');
|
||||
if (!in_array($type, $this->acceptedTypes, true)) {
|
||||
return [
|
||||
'job_status' => 'rejected',
|
||||
'error' => 'Unsupported command type',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'job_status' => 'queued',
|
||||
'job_id' => uniqid('job_', true),
|
||||
'tenant_id' => $commandEnvelope['tenant_id'] ?? null,
|
||||
'type' => $type,
|
||||
'idempotency_key' => $commandEnvelope['idempotency_key'] ?? null,
|
||||
'args' => $commandEnvelope['args'] ?? [],
|
||||
'message' => 'Command accepted for execution plane dispatch',
|
||||
];
|
||||
}
|
||||
}
|
||||
25
panel-api/app/Modules/Jobs/JobController.php
Normal file
25
panel-api/app/Modules/Jobs/JobController.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Jobs;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class JobController extends Controller
|
||||
{
|
||||
public function __construct(private readonly CommandOrchestrator $orchestrator)
|
||||
{
|
||||
}
|
||||
|
||||
public function dispatch(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
$payload = $request->all();
|
||||
$payload['tenant_id'] = $tenantId;
|
||||
|
||||
$result = $this->orchestrator->dispatch($payload);
|
||||
|
||||
return response()->json(['data' => $result], 202);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Marketplace;
|
||||
|
||||
class MarketplaceCommandFactory
|
||||
{
|
||||
public function installPlugin(string $tenantId, array $input): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => $tenantId,
|
||||
'type' => 'PLUGIN_INSTALL',
|
||||
'idempotency_key' => $input['idempotency_key'] ?? uniqid('plugin_install_', true),
|
||||
'args' => $input,
|
||||
];
|
||||
}
|
||||
|
||||
public function updatePlugin(string $tenantId, array $input): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => $tenantId,
|
||||
'type' => 'PLUGIN_UPDATE',
|
||||
'idempotency_key' => $input['idempotency_key'] ?? uniqid('plugin_update_', true),
|
||||
'args' => $input,
|
||||
];
|
||||
}
|
||||
|
||||
public function removePlugin(string $tenantId, array $input): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => $tenantId,
|
||||
'type' => 'PLUGIN_REMOVE',
|
||||
'idempotency_key' => $input['idempotency_key'] ?? uniqid('plugin_remove_', true),
|
||||
'args' => $input,
|
||||
];
|
||||
}
|
||||
}
|
||||
42
panel-api/app/Modules/Marketplace/MarketplaceController.php
Normal file
42
panel-api/app/Modules/Marketplace/MarketplaceController.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Marketplace;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class MarketplaceController extends Controller
|
||||
{
|
||||
public function __construct(private readonly MarketplaceWorkflowService $workflow)
|
||||
{
|
||||
}
|
||||
|
||||
public function catalog(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'items' => [],
|
||||
'meta' => ['source' => 'placeholder-market-catalog'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function install(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
return response()->json(['data' => $this->workflow->install($tenantId, $request->all())], 202);
|
||||
}
|
||||
|
||||
public function update(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
return response()->json(['data' => $this->workflow->update($tenantId, $request->all())], 202);
|
||||
}
|
||||
|
||||
public function remove(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
return response()->json(['data' => $this->workflow->remove($tenantId, $request->all())], 202);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Marketplace;
|
||||
|
||||
use App\Modules\Jobs\CommandOrchestrator;
|
||||
|
||||
class MarketplaceWorkflowService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CommandOrchestrator $orchestrator,
|
||||
private readonly MarketplaceCommandFactory $commands
|
||||
) {
|
||||
}
|
||||
|
||||
public function install(string $tenantId, array $input): array
|
||||
{
|
||||
return $this->orchestrator->dispatch($this->commands->installPlugin($tenantId, $input));
|
||||
}
|
||||
|
||||
public function update(string $tenantId, array $input): array
|
||||
{
|
||||
return $this->orchestrator->dispatch($this->commands->updatePlugin($tenantId, $input));
|
||||
}
|
||||
|
||||
public function remove(string $tenantId, array $input): array
|
||||
{
|
||||
return $this->orchestrator->dispatch($this->commands->removePlugin($tenantId, $input));
|
||||
}
|
||||
}
|
||||
20
panel-api/app/Modules/Monitoring/MetricsIngestController.php
Normal file
20
panel-api/app/Modules/Monitoring/MetricsIngestController.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Monitoring;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class MetricsIngestController extends Controller
|
||||
{
|
||||
public function ingest(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'accepted' => true,
|
||||
'points' => is_array($request->input('points')) ? count($request->input('points')) : 0,
|
||||
],
|
||||
], 202);
|
||||
}
|
||||
}
|
||||
43
panel-api/app/Modules/Monitoring/MonitoringController.php
Normal file
43
panel-api/app/Modules/Monitoring/MonitoringController.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Monitoring;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class MonitoringController extends Controller
|
||||
{
|
||||
public function liveServerMetrics(string $serverId): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'server_id' => $serverId,
|
||||
'cpu_percent' => null,
|
||||
'memory_percent' => null,
|
||||
'disk_percent' => null,
|
||||
'network' => ['in' => null, 'out' => null],
|
||||
'status' => 'streaming',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function createAlertRule(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'status' => 'created',
|
||||
'rule' => $request->all(),
|
||||
],
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function listAlerts(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'items' => [],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
26
panel-api/app/Modules/Ops/BackupController.php
Normal file
26
panel-api/app/Modules/Ops/BackupController.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Ops;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class BackupController extends Controller
|
||||
{
|
||||
public function __construct(private readonly OpsWorkflowService $workflow)
|
||||
{
|
||||
}
|
||||
|
||||
public function run(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
return response()->json(['data' => $this->workflow->backupRun($tenantId, $request->all())], 202);
|
||||
}
|
||||
|
||||
public function restore(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
return response()->json(['data' => $this->workflow->backupRestore($tenantId, $request->all())], 202);
|
||||
}
|
||||
}
|
||||
32
panel-api/app/Modules/Ops/CronController.php
Normal file
32
panel-api/app/Modules/Ops/CronController.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Ops;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class CronController extends Controller
|
||||
{
|
||||
public function __construct(private readonly OpsWorkflowService $workflow)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
return response()->json(['data' => $this->workflow->cronList($tenantId, $request->all())], 202);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
return response()->json(['data' => $this->workflow->cronCreate($tenantId, $request->all())], 202);
|
||||
}
|
||||
|
||||
public function destroy(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
return response()->json(['data' => $this->workflow->cronDelete($tenantId, $request->all())], 202);
|
||||
}
|
||||
}
|
||||
32
panel-api/app/Modules/Ops/FileOpsController.php
Normal file
32
panel-api/app/Modules/Ops/FileOpsController.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Ops;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class FileOpsController extends Controller
|
||||
{
|
||||
public function __construct(private readonly OpsWorkflowService $workflow)
|
||||
{
|
||||
}
|
||||
|
||||
public function list(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
return response()->json(['data' => $this->workflow->fileList($tenantId, $request->all())], 202);
|
||||
}
|
||||
|
||||
public function read(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
return response()->json(['data' => $this->workflow->fileRead($tenantId, $request->all())], 202);
|
||||
}
|
||||
|
||||
public function write(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
return response()->json(['data' => $this->workflow->fileWrite($tenantId, $request->all())], 202);
|
||||
}
|
||||
}
|
||||
32
panel-api/app/Modules/Ops/FirewallController.php
Normal file
32
panel-api/app/Modules/Ops/FirewallController.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Ops;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class FirewallController extends Controller
|
||||
{
|
||||
public function __construct(private readonly OpsWorkflowService $workflow)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
return response()->json(['data' => $this->workflow->firewallList($tenantId, $request->all())], 202);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
return response()->json(['data' => $this->workflow->firewallAdd($tenantId, $request->all())], 202);
|
||||
}
|
||||
|
||||
public function destroy(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
return response()->json(['data' => $this->workflow->firewallDelete($tenantId, $request->all())], 202);
|
||||
}
|
||||
}
|
||||
71
panel-api/app/Modules/Ops/OpsCommandFactory.php
Normal file
71
panel-api/app/Modules/Ops/OpsCommandFactory.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Ops;
|
||||
|
||||
class OpsCommandFactory
|
||||
{
|
||||
public function fileList(string $tenantId, array $input): array
|
||||
{
|
||||
return $this->cmd($tenantId, 'FILE_LIST', 'file_list_', $input);
|
||||
}
|
||||
|
||||
public function fileRead(string $tenantId, array $input): array
|
||||
{
|
||||
return $this->cmd($tenantId, 'FILE_READ', 'file_read_', $input);
|
||||
}
|
||||
|
||||
public function fileWrite(string $tenantId, array $input): array
|
||||
{
|
||||
return $this->cmd($tenantId, 'FILE_WRITE', 'file_write_', $input);
|
||||
}
|
||||
|
||||
public function cronList(string $tenantId, array $input): array
|
||||
{
|
||||
return $this->cmd($tenantId, 'CRON_LIST', 'cron_list_', $input);
|
||||
}
|
||||
|
||||
public function cronCreate(string $tenantId, array $input): array
|
||||
{
|
||||
return $this->cmd($tenantId, 'CRON_CREATE', 'cron_create_', $input);
|
||||
}
|
||||
|
||||
public function cronDelete(string $tenantId, array $input): array
|
||||
{
|
||||
return $this->cmd($tenantId, 'CRON_DELETE', 'cron_delete_', $input);
|
||||
}
|
||||
|
||||
public function firewallList(string $tenantId, array $input): array
|
||||
{
|
||||
return $this->cmd($tenantId, 'FIREWALL_RULE_LIST', 'fw_list_', $input);
|
||||
}
|
||||
|
||||
public function firewallAdd(string $tenantId, array $input): array
|
||||
{
|
||||
return $this->cmd($tenantId, 'FIREWALL_RULE_ADD', 'fw_add_', $input);
|
||||
}
|
||||
|
||||
public function firewallDelete(string $tenantId, array $input): array
|
||||
{
|
||||
return $this->cmd($tenantId, 'FIREWALL_RULE_DELETE', 'fw_delete_', $input);
|
||||
}
|
||||
|
||||
public function backupRun(string $tenantId, array $input): array
|
||||
{
|
||||
return $this->cmd($tenantId, 'BACKUP_RUN', 'backup_run_', $input);
|
||||
}
|
||||
|
||||
public function backupRestore(string $tenantId, array $input): array
|
||||
{
|
||||
return $this->cmd($tenantId, 'BACKUP_RESTORE', 'backup_restore_', $input);
|
||||
}
|
||||
|
||||
private function cmd(string $tenantId, string $type, string $prefix, array $input): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => $tenantId,
|
||||
'type' => $type,
|
||||
'idempotency_key' => $input['idempotency_key'] ?? uniqid($prefix, true),
|
||||
'args' => $input,
|
||||
];
|
||||
}
|
||||
}
|
||||
26
panel-api/app/Modules/Ops/OpsWorkflowService.php
Normal file
26
panel-api/app/Modules/Ops/OpsWorkflowService.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Ops;
|
||||
|
||||
use App\Modules\Jobs\CommandOrchestrator;
|
||||
|
||||
class OpsWorkflowService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CommandOrchestrator $orchestrator,
|
||||
private readonly OpsCommandFactory $commands
|
||||
) {
|
||||
}
|
||||
|
||||
public function fileList(string $tenantId, array $input): array { return $this->orchestrator->dispatch($this->commands->fileList($tenantId, $input)); }
|
||||
public function fileRead(string $tenantId, array $input): array { return $this->orchestrator->dispatch($this->commands->fileRead($tenantId, $input)); }
|
||||
public function fileWrite(string $tenantId, array $input): array { return $this->orchestrator->dispatch($this->commands->fileWrite($tenantId, $input)); }
|
||||
public function cronList(string $tenantId, array $input): array { return $this->orchestrator->dispatch($this->commands->cronList($tenantId, $input)); }
|
||||
public function cronCreate(string $tenantId, array $input): array { return $this->orchestrator->dispatch($this->commands->cronCreate($tenantId, $input)); }
|
||||
public function cronDelete(string $tenantId, array $input): array { return $this->orchestrator->dispatch($this->commands->cronDelete($tenantId, $input)); }
|
||||
public function firewallList(string $tenantId, array $input): array { return $this->orchestrator->dispatch($this->commands->firewallList($tenantId, $input)); }
|
||||
public function firewallAdd(string $tenantId, array $input): array { return $this->orchestrator->dispatch($this->commands->firewallAdd($tenantId, $input)); }
|
||||
public function firewallDelete(string $tenantId, array $input): array { return $this->orchestrator->dispatch($this->commands->firewallDelete($tenantId, $input)); }
|
||||
public function backupRun(string $tenantId, array $input): array { return $this->orchestrator->dispatch($this->commands->backupRun($tenantId, $input)); }
|
||||
public function backupRestore(string $tenantId, array $input): array { return $this->orchestrator->dispatch($this->commands->backupRestore($tenantId, $input)); }
|
||||
}
|
||||
29
panel-api/app/Modules/PublicApi/PublicApiController.php
Normal file
29
panel-api/app/Modules/PublicApi/PublicApiController.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\PublicApi;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class PublicApiController extends Controller
|
||||
{
|
||||
public function __construct(private readonly PublicTokenService $tokens)
|
||||
{
|
||||
}
|
||||
|
||||
public function issueToken(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => $this->tokens->issue($request->all()),
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function introspect(Request $request): JsonResponse
|
||||
{
|
||||
$token = (string) $request->input('token', '');
|
||||
return response()->json([
|
||||
'data' => $this->tokens->introspect($token),
|
||||
]);
|
||||
}
|
||||
}
|
||||
23
panel-api/app/Modules/PublicApi/PublicTokenService.php
Normal file
23
panel-api/app/Modules/PublicApi/PublicTokenService.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\PublicApi;
|
||||
|
||||
class PublicTokenService
|
||||
{
|
||||
public function issue(array $input): array
|
||||
{
|
||||
return [
|
||||
'token' => 'public-token-placeholder',
|
||||
'scopes' => $input['scopes'] ?? [],
|
||||
'expires_in' => 3600,
|
||||
];
|
||||
}
|
||||
|
||||
public function introspect(string $token): array
|
||||
{
|
||||
return [
|
||||
'active' => $token !== '',
|
||||
'scopes' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
49
panel-api/app/Modules/Rbac/RbacController.php
Normal file
49
panel-api/app/Modules/Rbac/RbacController.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Rbac;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class RbacController extends Controller
|
||||
{
|
||||
public function __construct(private readonly ScopeEvaluator $evaluator)
|
||||
{
|
||||
}
|
||||
|
||||
public function roles(): JsonResponse
|
||||
{
|
||||
return response()->json(['data' => []]);
|
||||
}
|
||||
|
||||
public function createRole(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json(['data' => ['status' => 'created']], 201);
|
||||
}
|
||||
|
||||
public function attachPermissions(Request $request, string $role): JsonResponse
|
||||
{
|
||||
return response()->json(['data' => ['role_id' => $role, 'status' => 'updated']]);
|
||||
}
|
||||
|
||||
public function assignRoles(Request $request, string $user): JsonResponse
|
||||
{
|
||||
return response()->json(['data' => ['user_id' => $user, 'status' => 'updated']]);
|
||||
}
|
||||
|
||||
public function checkAccess(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->all();
|
||||
$allowed = $this->evaluator->isAllowed(
|
||||
$payload['grants'] ?? [],
|
||||
(string) ($payload['action'] ?? ''),
|
||||
(string) ($payload['resource_type'] ?? ''),
|
||||
isset($payload['resource_id']) ? (string) $payload['resource_id'] : null
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'data' => ['allowed' => $allowed],
|
||||
]);
|
||||
}
|
||||
}
|
||||
30
panel-api/app/Modules/Rbac/ScopeEvaluator.php
Normal file
30
panel-api/app/Modules/Rbac/ScopeEvaluator.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Rbac;
|
||||
|
||||
class ScopeEvaluator
|
||||
{
|
||||
public function isAllowed(array $grants, string $action, string $resourceType, ?string $resourceId = null): bool
|
||||
{
|
||||
foreach ($grants as $grant) {
|
||||
$grantAction = $grant['action'] ?? null;
|
||||
$grantResourceType = $grant['resource_type'] ?? null;
|
||||
$grantResourceId = $grant['resource_id'] ?? null;
|
||||
$effect = $grant['effect'] ?? 'allow';
|
||||
|
||||
$matches = ($grantAction === '*' || $grantAction === $action)
|
||||
&& ($grantResourceType === '*' || $grantResourceType === $resourceType)
|
||||
&& ($grantResourceId === null || $grantResourceId === $resourceId);
|
||||
|
||||
if ($matches && $effect === 'deny') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($matches && $effect === 'allow') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
16
panel-api/app/Modules/SaaS/BillingHooksService.php
Normal file
16
panel-api/app/Modules/SaaS/BillingHooksService.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\SaaS;
|
||||
|
||||
class BillingHooksService
|
||||
{
|
||||
public function emitUsageEvent(string $tenantId, string $eventType, array $payload): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => $tenantId,
|
||||
'event_type' => $eventType,
|
||||
'status' => 'queued',
|
||||
'payload' => $payload,
|
||||
];
|
||||
}
|
||||
}
|
||||
18
panel-api/app/Modules/SaaS/QuotaService.php
Normal file
18
panel-api/app/Modules/SaaS/QuotaService.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\SaaS;
|
||||
|
||||
class QuotaService
|
||||
{
|
||||
public function check(string $tenantId, string $resource, int $requested = 1): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => $tenantId,
|
||||
'resource' => $resource,
|
||||
'requested' => $requested,
|
||||
'allowed' => true,
|
||||
'limit' => null,
|
||||
'current_usage' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
38
panel-api/app/Modules/SaaS/SaaSController.php
Normal file
38
panel-api/app/Modules/SaaS/SaaSController.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\SaaS;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class SaaSController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly QuotaService $quota,
|
||||
private readonly BillingHooksService $billing
|
||||
) {
|
||||
}
|
||||
|
||||
public function checkQuota(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
$resource = (string) $request->input('resource', '');
|
||||
$requested = (int) $request->input('requested', 1);
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->quota->check($tenantId, $resource, $requested),
|
||||
]);
|
||||
}
|
||||
|
||||
public function usageEvent(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
$eventType = (string) $request->input('event_type', 'generic');
|
||||
$payload = (array) $request->input('payload', []);
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->billing->emitUsageEvent($tenantId, $eventType, $payload),
|
||||
], 202);
|
||||
}
|
||||
}
|
||||
56
panel-api/app/Modules/Server/ServerController.php
Normal file
56
panel-api/app/Modules/Server/ServerController.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Server;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class ServerController extends Controller
|
||||
{
|
||||
public function __construct(private readonly ServerRepository $servers)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
return response()->json([
|
||||
'data' => $this->servers->listByTenant($tenantId),
|
||||
'meta' => ['tenant_id' => $tenantId],
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
$server = $this->servers->create([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => (string) $request->input('name', ''),
|
||||
'id' => (string) $request->input('id', ''),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'data' => $server,
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function show(string $server): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => ['id' => $server],
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, string $server): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => ['id' => $server, 'status' => 'updated'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(string $server): JsonResponse
|
||||
{
|
||||
return response()->json([], 204);
|
||||
}
|
||||
}
|
||||
25
panel-api/app/Modules/Server/ServerRepository.php
Normal file
25
panel-api/app/Modules/Server/ServerRepository.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Server;
|
||||
|
||||
class ServerRepository
|
||||
{
|
||||
/**
|
||||
* Placeholder persistence adapter.
|
||||
* Replace with Eloquent model queries in full Laravel bootstrap.
|
||||
*/
|
||||
public function listByTenant(string $tenantId): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function create(array $attributes): array
|
||||
{
|
||||
return [
|
||||
'id' => $attributes['id'] ?? null,
|
||||
'tenant_id' => $attributes['tenant_id'] ?? null,
|
||||
'name' => $attributes['name'] ?? null,
|
||||
'status' => 'provisioning',
|
||||
];
|
||||
}
|
||||
}
|
||||
107
panel-api/app/Modules/Site/HostingCommandFactory.php
Normal file
107
panel-api/app/Modules/Site/HostingCommandFactory.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Site;
|
||||
|
||||
class HostingCommandFactory
|
||||
{
|
||||
public function buildCreateSite(string $tenantId, array $input, string $webserver): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => $tenantId,
|
||||
'type' => 'SITE_CREATE',
|
||||
'idempotency_key' => $input['idempotency_key'] ?? uniqid('site_create_', true),
|
||||
'args' => [
|
||||
'server_id' => $input['server_id'] ?? null,
|
||||
'site_name' => $input['site_name'] ?? null,
|
||||
'domain' => $input['domain'] ?? null,
|
||||
'root_path' => $input['root_path'] ?? null,
|
||||
'webserver' => $webserver,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function buildUpdateSite(string $tenantId, string $siteId, array $input): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => $tenantId,
|
||||
'type' => 'SITE_UPDATE',
|
||||
'idempotency_key' => $input['idempotency_key'] ?? uniqid('site_update_', true),
|
||||
'args' => [
|
||||
'site_id' => $siteId,
|
||||
'server_id' => $input['server_id'] ?? null,
|
||||
'changes' => $input['changes'] ?? [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function buildDeleteSite(string $tenantId, string $siteId, array $input): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => $tenantId,
|
||||
'type' => 'SITE_DELETE',
|
||||
'idempotency_key' => $input['idempotency_key'] ?? uniqid('site_delete_', true),
|
||||
'args' => [
|
||||
'site_id' => $siteId,
|
||||
'server_id' => $input['server_id'] ?? null,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function buildAddDomain(string $tenantId, string $siteId, array $input): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => $tenantId,
|
||||
'type' => 'DOMAIN_ADD',
|
||||
'idempotency_key' => $input['idempotency_key'] ?? uniqid('domain_add_', true),
|
||||
'args' => [
|
||||
'site_id' => $siteId,
|
||||
'server_id' => $input['server_id'] ?? null,
|
||||
'domain' => $input['domain'] ?? null,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function buildRemoveDomain(string $tenantId, string $siteId, string $domainId, array $input): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => $tenantId,
|
||||
'type' => 'DOMAIN_REMOVE',
|
||||
'idempotency_key' => $input['idempotency_key'] ?? uniqid('domain_remove_', true),
|
||||
'args' => [
|
||||
'site_id' => $siteId,
|
||||
'domain_id' => $domainId,
|
||||
'server_id' => $input['server_id'] ?? null,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function buildIssueSsl(string $tenantId, array $input): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => $tenantId,
|
||||
'type' => 'SSL_ISSUE',
|
||||
'idempotency_key' => $input['idempotency_key'] ?? uniqid('ssl_issue_', true),
|
||||
'args' => $input,
|
||||
];
|
||||
}
|
||||
|
||||
public function buildApplySsl(string $tenantId, array $input): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => $tenantId,
|
||||
'type' => 'SSL_APPLY',
|
||||
'idempotency_key' => $input['idempotency_key'] ?? uniqid('ssl_apply_', true),
|
||||
'args' => $input,
|
||||
];
|
||||
}
|
||||
|
||||
public function buildRenewSsl(string $tenantId, array $input): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => $tenantId,
|
||||
'type' => 'SSL_RENEW',
|
||||
'idempotency_key' => $input['idempotency_key'] ?? uniqid('ssl_renew_', true),
|
||||
'args' => $input,
|
||||
];
|
||||
}
|
||||
}
|
||||
53
panel-api/app/Modules/Site/SiteController.php
Normal file
53
panel-api/app/Modules/Site/SiteController.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Site;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class SiteController extends Controller
|
||||
{
|
||||
public function __construct(private readonly SiteWorkflowService $workflow)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(): JsonResponse { return response()->json(['data' => []]); }
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
$result = $this->workflow->createSite($tenantId, $request->all());
|
||||
return response()->json(['data' => $result], 202);
|
||||
}
|
||||
|
||||
public function show(string $site): JsonResponse { return response()->json(['data' => ['id' => $site]]); }
|
||||
|
||||
public function update(Request $request, string $site): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
$result = $this->workflow->updateSite($tenantId, $site, $request->all());
|
||||
return response()->json(['data' => $result], 202);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, string $site): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
$result = $this->workflow->deleteSite($tenantId, $site, $request->all());
|
||||
return response()->json(['data' => $result], 202);
|
||||
}
|
||||
|
||||
public function addDomain(Request $request, string $site): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
$result = $this->workflow->addDomain($tenantId, $site, $request->all());
|
||||
return response()->json(['data' => $result], 202);
|
||||
}
|
||||
|
||||
public function removeDomain(Request $request, string $site, string $domain): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
$result = $this->workflow->removeDomain($tenantId, $site, $domain, $request->all());
|
||||
return response()->json(['data' => $result], 202);
|
||||
}
|
||||
}
|
||||
69
panel-api/app/Modules/Site/SiteWorkflowService.php
Normal file
69
panel-api/app/Modules/Site/SiteWorkflowService.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Site;
|
||||
|
||||
use App\Modules\Jobs\CommandOrchestrator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class SiteWorkflowService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CommandOrchestrator $orchestrator,
|
||||
private readonly HostingCommandFactory $commandFactory,
|
||||
private readonly WebserverProfileResolver $webserverResolver
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function createSite(string $tenantId, array $input): array
|
||||
{
|
||||
$webserver = $this->webserverResolver->normalize((string) ($input['webserver'] ?? 'nginx'));
|
||||
$command = $this->commandFactory->buildCreateSite($tenantId, $input, $webserver);
|
||||
return $this->orchestrator->dispatch($command);
|
||||
}
|
||||
|
||||
public function updateSite(string $tenantId, string $siteId, array $input): array
|
||||
{
|
||||
$command = $this->commandFactory->buildUpdateSite($tenantId, $siteId, $input);
|
||||
return $this->orchestrator->dispatch($command);
|
||||
}
|
||||
|
||||
public function deleteSite(string $tenantId, string $siteId, array $input = []): array
|
||||
{
|
||||
$command = $this->commandFactory->buildDeleteSite($tenantId, $siteId, $input);
|
||||
return $this->orchestrator->dispatch($command);
|
||||
}
|
||||
|
||||
public function addDomain(string $tenantId, string $siteId, array $input): array
|
||||
{
|
||||
if (empty($input['domain'])) {
|
||||
throw new InvalidArgumentException('Domain is required');
|
||||
}
|
||||
$command = $this->commandFactory->buildAddDomain($tenantId, $siteId, $input);
|
||||
return $this->orchestrator->dispatch($command);
|
||||
}
|
||||
|
||||
public function removeDomain(string $tenantId, string $siteId, string $domainId, array $input = []): array
|
||||
{
|
||||
$command = $this->commandFactory->buildRemoveDomain($tenantId, $siteId, $domainId, $input);
|
||||
return $this->orchestrator->dispatch($command);
|
||||
}
|
||||
|
||||
public function issueSsl(string $tenantId, array $input): array
|
||||
{
|
||||
$command = $this->commandFactory->buildIssueSsl($tenantId, $input);
|
||||
return $this->orchestrator->dispatch($command);
|
||||
}
|
||||
|
||||
public function applySsl(string $tenantId, array $input): array
|
||||
{
|
||||
$command = $this->commandFactory->buildApplySsl($tenantId, $input);
|
||||
return $this->orchestrator->dispatch($command);
|
||||
}
|
||||
|
||||
public function renewSsl(string $tenantId, array $input): array
|
||||
{
|
||||
$command = $this->commandFactory->buildRenewSsl($tenantId, $input);
|
||||
return $this->orchestrator->dispatch($command);
|
||||
}
|
||||
}
|
||||
23
panel-api/app/Modules/Site/WebserverProfileResolver.php
Normal file
23
panel-api/app/Modules/Site/WebserverProfileResolver.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Site;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class WebserverProfileResolver
|
||||
{
|
||||
/**
|
||||
* Normalize webserver name to an allowed adapter key.
|
||||
*/
|
||||
public function normalize(string $webserver): string
|
||||
{
|
||||
$normalized = strtolower(trim($webserver));
|
||||
|
||||
return match ($normalized) {
|
||||
'nginx' => 'nginx',
|
||||
'apache', 'httpd' => 'apache',
|
||||
'openlitespeed', 'ols' => 'openlitespeed',
|
||||
default => throw new InvalidArgumentException('Unsupported webserver profile'),
|
||||
};
|
||||
}
|
||||
}
|
||||
36
panel-api/app/Modules/Ssl/SslController.php
Normal file
36
panel-api/app/Modules/Ssl/SslController.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Ssl;
|
||||
|
||||
use App\Modules\Site\SiteWorkflowService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class SslController extends Controller
|
||||
{
|
||||
public function __construct(private readonly SiteWorkflowService $workflow)
|
||||
{
|
||||
}
|
||||
|
||||
public function issue(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
$result = $this->workflow->issueSsl($tenantId, $request->all());
|
||||
return response()->json(['data' => $result], 202);
|
||||
}
|
||||
|
||||
public function apply(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
$result = $this->workflow->applySsl($tenantId, $request->all());
|
||||
return response()->json(['data' => $result], 202);
|
||||
}
|
||||
|
||||
public function renew(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = (string) $request->attributes->get('tenant_id', '');
|
||||
$result = $this->workflow->renewSsl($tenantId, $request->all());
|
||||
return response()->json(['data' => $result], 202);
|
||||
}
|
||||
}
|
||||
25
panel-api/app/Modules/Tenant/TenantController.php
Normal file
25
panel-api/app/Modules/Tenant/TenantController.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Tenant;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class TenantController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json(['data' => []]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json(['data' => ['status' => 'created']], 201);
|
||||
}
|
||||
|
||||
public function show(string $tenant): JsonResponse
|
||||
{
|
||||
return response()->json(['data' => ['id' => $tenant]]);
|
||||
}
|
||||
}
|
||||
35
panel-api/app/Modules/Webhook/WebhookController.php
Normal file
35
panel-api/app/Modules/Webhook/WebhookController.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Webhook;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class WebhookController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json(['data' => ['items' => []]]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'status' => 'created',
|
||||
'endpoint' => $request->all(),
|
||||
],
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function testDelivery(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'status' => 'queued',
|
||||
'target' => $request->input('url'),
|
||||
],
|
||||
], 202);
|
||||
}
|
||||
}
|
||||
24
panel-api/public/index.php
Normal file
24
panel-api/public/index.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($path === '/health') {
|
||||
echo json_encode([
|
||||
'status' => 'ok',
|
||||
'service' => 'yakpanel-api',
|
||||
'mode' => 'scaffold',
|
||||
'time' => gmdate(DATE_ATOM),
|
||||
], JSON_PRETTY_PRINT);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'ok',
|
||||
'service' => 'yakpanel-api',
|
||||
'message' => 'YakPanel control-plane scaffold is running',
|
||||
'path' => $path,
|
||||
], JSON_PRETTY_PRINT);
|
||||
20
panel-api/routes/api_v1/hosting.php
Normal file
20
panel-api/routes/api_v1/hosting.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Site\SiteController;
|
||||
use App\Modules\Ssl\SslController;
|
||||
|
||||
Route::prefix('v1')->middleware(['auth:sanctum'])->group(function (): void {
|
||||
Route::get('/sites', [SiteController::class, 'index']);
|
||||
Route::post('/sites', [SiteController::class, 'store']);
|
||||
Route::get('/sites/{site}', [SiteController::class, 'show']);
|
||||
Route::patch('/sites/{site}', [SiteController::class, 'update']);
|
||||
Route::delete('/sites/{site}', [SiteController::class, 'destroy']);
|
||||
|
||||
Route::post('/sites/{site}/domains', [SiteController::class, 'addDomain']);
|
||||
Route::delete('/sites/{site}/domains/{domain}', [SiteController::class, 'removeDomain']);
|
||||
|
||||
Route::post('/ssl/issue', [SslController::class, 'issue']);
|
||||
Route::post('/ssl/apply', [SslController::class, 'apply']);
|
||||
Route::post('/ssl/renew', [SslController::class, 'renew']);
|
||||
});
|
||||
26
panel-api/routes/api_v1/identity.php
Normal file
26
panel-api/routes/api_v1/identity.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Auth\AuthController;
|
||||
use App\Modules\Tenant\TenantController;
|
||||
use App\Modules\Rbac\RbacController;
|
||||
|
||||
Route::prefix('v1')->group(function (): void {
|
||||
Route::post('/auth/login', [AuthController::class, 'login']);
|
||||
Route::post('/auth/refresh', [AuthController::class, 'refresh']);
|
||||
|
||||
Route::middleware(['auth:sanctum'])->group(function (): void {
|
||||
Route::get('/auth/me', [AuthController::class, 'me']);
|
||||
Route::post('/auth/logout', [AuthController::class, 'logout']);
|
||||
|
||||
Route::get('/tenants', [TenantController::class, 'index']);
|
||||
Route::post('/tenants', [TenantController::class, 'store']);
|
||||
Route::get('/tenants/{tenant}', [TenantController::class, 'show']);
|
||||
|
||||
Route::get('/rbac/roles', [RbacController::class, 'roles']);
|
||||
Route::post('/rbac/roles', [RbacController::class, 'createRole']);
|
||||
Route::post('/rbac/roles/{role}/permissions', [RbacController::class, 'attachPermissions']);
|
||||
Route::post('/rbac/users/{user}/roles', [RbacController::class, 'assignRoles']);
|
||||
Route::post('/rbac/check', [RbacController::class, 'checkAccess']);
|
||||
});
|
||||
});
|
||||
8
panel-api/routes/api_v1/jobs.php
Normal file
8
panel-api/routes/api_v1/jobs.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Jobs\JobController;
|
||||
|
||||
Route::prefix('v1')->middleware(['auth:sanctum'])->group(function (): void {
|
||||
Route::post('/jobs/dispatch', [JobController::class, 'dispatch']);
|
||||
});
|
||||
11
panel-api/routes/api_v1/marketplace.php
Normal file
11
panel-api/routes/api_v1/marketplace.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Marketplace\MarketplaceController;
|
||||
|
||||
Route::prefix('v1')->middleware(['auth:sanctum'])->group(function (): void {
|
||||
Route::get('/plugin-market', [MarketplaceController::class, 'catalog']);
|
||||
Route::post('/plugins/install', [MarketplaceController::class, 'install']);
|
||||
Route::post('/plugins/update', [MarketplaceController::class, 'update']);
|
||||
Route::post('/plugins/remove', [MarketplaceController::class, 'remove']);
|
||||
});
|
||||
15
panel-api/routes/api_v1/monitoring.php
Normal file
15
panel-api/routes/api_v1/monitoring.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Monitoring\MonitoringController;
|
||||
use App\Modules\Monitoring\MetricsIngestController;
|
||||
|
||||
Route::prefix('v1')->middleware(['auth:sanctum'])->group(function (): void {
|
||||
Route::get('/metrics/servers/{serverId}/live', [MonitoringController::class, 'liveServerMetrics']);
|
||||
Route::post('/alerts/rules', [MonitoringController::class, 'createAlertRule']);
|
||||
Route::get('/alerts', [MonitoringController::class, 'listAlerts']);
|
||||
});
|
||||
|
||||
Route::prefix('v1')->group(function (): void {
|
||||
Route::post('/metrics/ingest', [MetricsIngestController::class, 'ingest']);
|
||||
});
|
||||
24
panel-api/routes/api_v1/ops.php
Normal file
24
panel-api/routes/api_v1/ops.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Ops\FileOpsController;
|
||||
use App\Modules\Ops\CronController;
|
||||
use App\Modules\Ops\FirewallController;
|
||||
use App\Modules\Ops\BackupController;
|
||||
|
||||
Route::prefix('v1')->middleware(['auth:sanctum'])->group(function (): void {
|
||||
Route::post('/files/list', [FileOpsController::class, 'list']);
|
||||
Route::post('/files/read', [FileOpsController::class, 'read']);
|
||||
Route::post('/files/write', [FileOpsController::class, 'write']);
|
||||
|
||||
Route::get('/cron/jobs', [CronController::class, 'index']);
|
||||
Route::post('/cron/jobs', [CronController::class, 'store']);
|
||||
Route::delete('/cron/jobs', [CronController::class, 'destroy']);
|
||||
|
||||
Route::get('/firewall/rules', [FirewallController::class, 'index']);
|
||||
Route::post('/firewall/rules', [FirewallController::class, 'store']);
|
||||
Route::delete('/firewall/rules', [FirewallController::class, 'destroy']);
|
||||
|
||||
Route::post('/backups/run', [BackupController::class, 'run']);
|
||||
Route::post('/backups/restore', [BackupController::class, 'restore']);
|
||||
});
|
||||
9
panel-api/routes/api_v1/public.php
Normal file
9
panel-api/routes/api_v1/public.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\PublicApi\PublicApiController;
|
||||
|
||||
Route::prefix('v1/public')->group(function (): void {
|
||||
Route::post('/tokens', [PublicApiController::class, 'issueToken']);
|
||||
Route::post('/tokens/introspect', [PublicApiController::class, 'introspect']);
|
||||
});
|
||||
9
panel-api/routes/api_v1/saas.php
Normal file
9
panel-api/routes/api_v1/saas.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\SaaS\SaaSController;
|
||||
|
||||
Route::prefix('v1')->middleware(['auth:sanctum'])->group(function (): void {
|
||||
Route::post('/saas/quotas/check', [SaaSController::class, 'checkQuota']);
|
||||
Route::post('/saas/usage-events', [SaaSController::class, 'usageEvent']);
|
||||
});
|
||||
18
panel-api/routes/api_v1/servers.php
Normal file
18
panel-api/routes/api_v1/servers.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Server\ServerController;
|
||||
use App\Modules\Agents\AgentController;
|
||||
|
||||
Route::prefix('v1')->middleware(['auth:sanctum'])->group(function (): void {
|
||||
Route::get('/servers', [ServerController::class, 'index']);
|
||||
Route::post('/servers', [ServerController::class, 'store']);
|
||||
Route::get('/servers/{server}', [ServerController::class, 'show']);
|
||||
Route::patch('/servers/{server}', [ServerController::class, 'update']);
|
||||
Route::delete('/servers/{server}', [ServerController::class, 'destroy']);
|
||||
|
||||
Route::post('/servers/{server}/enrollment-token', [AgentController::class, 'issueEnrollmentToken']);
|
||||
Route::get('/servers/{server}/capabilities', [AgentController::class, 'capabilities']);
|
||||
Route::get('/servers/{server}/sessions', [AgentController::class, 'sessions']);
|
||||
Route::post('/servers/{server}/heartbeat', [AgentController::class, 'heartbeat']);
|
||||
});
|
||||
10
panel-api/routes/api_v1/webhooks.php
Normal file
10
panel-api/routes/api_v1/webhooks.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Webhook\WebhookController;
|
||||
|
||||
Route::prefix('v1')->middleware(['auth:sanctum'])->group(function (): void {
|
||||
Route::get('/webhooks', [WebhookController::class, 'index']);
|
||||
Route::post('/webhooks', [WebhookController::class, 'store']);
|
||||
Route::post('/webhooks/test', [WebhookController::class, 'testDelivery']);
|
||||
});
|
||||
11
scripts/bootstrap-dev.sh
Normal file
11
scripts/bootstrap-dev.sh
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
cp -n .env.example .env || true
|
||||
docker compose --env-file .env -f docker-compose.yakpanel.yml up -d --build
|
||||
docker compose --env-file .env -f docker-compose.yakpanel.yml run --rm db-migrate
|
||||
docker compose --env-file .env -f docker-compose.yakpanel.yml ps
|
||||
|
||||
echo "YakPanel dev stack is ready."
|
||||
48
scripts/install-ubuntu.sh
Normal file
48
scripts/install-ubuntu.sh
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${EUID}" -eq 0 ]]; then
|
||||
echo "Run this script as a regular sudo-capable user, not root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[1/6] Updating apt package index..."
|
||||
sudo apt-get update -y
|
||||
|
||||
echo "[2/6] Installing required Ubuntu packages..."
|
||||
sudo apt-get install -y ca-certificates curl gnupg lsb-release make git
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "[3/6] Installing Docker Engine..."
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
sudo chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
echo \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
|
||||
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
else
|
||||
echo "[3/6] Docker already installed, skipping."
|
||||
fi
|
||||
|
||||
echo "[4/6] Ensuring current user can run Docker..."
|
||||
sudo usermod -aG docker "$USER"
|
||||
|
||||
echo "[5/6] Initializing YakPanel environment file..."
|
||||
cd "$(dirname "$0")/.."
|
||||
cp -n .env.example .env || true
|
||||
|
||||
echo "[6/6] Starting YakPanel dev stack..."
|
||||
docker compose --env-file .env -f docker-compose.yakpanel.yml up -d --build
|
||||
docker compose --env-file .env -f docker-compose.yakpanel.yml run --rm db-migrate
|
||||
|
||||
echo
|
||||
echo "YakPanel dev stack started."
|
||||
echo "API: http://localhost:8080/health"
|
||||
echo "PostgreSQL: localhost:5432"
|
||||
echo "Redis: localhost:6379"
|
||||
echo "NATS monitor: http://localhost:8222"
|
||||
echo "MinIO console: http://localhost:9001"
|
||||
echo
|
||||
echo "If Docker group membership was newly applied, re-login may be required."
|
||||
11
scripts/migrate-dev.sh
Normal file
11
scripts/migrate-dev.sh
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [[ ! -f .env ]]; then
|
||||
cp .env.example .env
|
||||
fi
|
||||
|
||||
docker compose --env-file .env -f docker-compose.yakpanel.yml run --rm db-migrate
|
||||
echo "YakPanel database migrations applied."
|
||||
Reference in New Issue
Block a user