new changes

This commit is contained in:
Niranjan
2026-04-07 20:29:49 +05:30
parent 8fe63c7cf4
commit 31fe556bb0
79 changed files with 2917 additions and 0 deletions

16
.env.example Normal file
View 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
View 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

View File

@@ -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`

View 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.

View 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);

View 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;
}
```

View 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.

View 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.

View 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

View 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.

View 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`

View 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.

View 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.

View 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);

View 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.

View 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.

View 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);

View 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.

View File

@@ -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.

View File

@@ -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.

View 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()
);

View 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.

View 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
View File

@@ -0,0 +1,5 @@
module yakpanel/control-plane-go
go 1.23
require github.com/redis/go-redis/v9 v9.6.1

View 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)
}

View File

@@ -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
}

View 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 }

View 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
View 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:

View 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
View 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
View 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.

View 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);
}
}

View 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,
]);
}
}

View 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,
];
}
}

View 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);
}
}

View 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',
];
}
}

View 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);
}
}

View File

@@ -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,
];
}
}

View 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);
}
}

View File

@@ -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));
}
}

View 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);
}
}

View 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' => [],
],
]);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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,
];
}
}

View 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)); }
}

View 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),
]);
}
}

View 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' => [],
];
}
}

View 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],
]);
}
}

View 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;
}
}

View 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,
];
}
}

View 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,
];
}
}

View 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);
}
}

View 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);
}
}

View 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',
];
}
}

View 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,
];
}
}

View 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);
}
}

View 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);
}
}

View 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'),
};
}
}

View 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);
}
}

View 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]]);
}
}

View 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);
}
}

View 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);

View 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']);
});

View 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']);
});
});

View 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']);
});

View 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']);
});

View 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']);
});

View 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']);
});

View 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']);
});

View 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']);
});

View 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']);
});

View 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
View 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
View 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
View 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."