功能:新增云管API与Web控制台并完成现场接入
This commit is contained in:
1 parent
18a9ce995d
commit
3fb7b4da8c
79 files changed
+17367
-3
No files matched your search
@@ -0,0 +1,59 @@
|
||||
CREATE TABLE IF NOT EXISTS platform_metadata (
|
||||
singleton boolean PRIMARY KEY DEFAULT true CHECK(singleton), mode text NOT NULL CHECK(mode IN ('demo','production'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS administrators (
|
||||
id text PRIMARY KEY, username text NOT NULL UNIQUE, display_name text NOT NULL,
|
||||
role text NOT NULL CHECK (role IN ('super_admin','operator','viewer')), password_hash text NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS admin_sessions (
|
||||
id text PRIMARY KEY, admin_id text NOT NULL REFERENCES administrators(id), expires_at timestamptz NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS desktops (
|
||||
id text PRIMARY KEY, name text NOT NULL, os text NOT NULL, node text NOT NULL, vmid integer NOT NULL,
|
||||
pool text NOT NULL DEFAULT '', power_state text NOT NULL, service_state text NOT NULL DEFAULT 'unknown',
|
||||
provider text NOT NULL CHECK (provider IN ('demo','proxmox')), endpoint text NOT NULL DEFAULT '', app text NOT NULL DEFAULT 'Desktop',
|
||||
width integer NOT NULL DEFAULT 2560, height integer NOT NULL DEFAULT 1440, fps integer NOT NULL DEFAULT 30,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE(provider,node,vmid)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS terminals (
|
||||
id text PRIMARY KEY, sn text NOT NULL UNIQUE, name text NOT NULL, group_name text NOT NULL DEFAULT '',
|
||||
status text NOT NULL DEFAULT 'unregistered', agent_version text NOT NULL DEFAULT '', last_seen timestamptz,
|
||||
session_desktop_id text REFERENCES desktops(id), config_version integer NOT NULL DEFAULT 1,
|
||||
bootstrap_hash text NOT NULL, token_hash text UNIQUE, fps double precision, rtt double precision
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id text PRIMARY KEY, username text NOT NULL UNIQUE, display_name text NOT NULL,
|
||||
enabled boolean NOT NULL DEFAULT true, terminal_id text UNIQUE REFERENCES terminals(id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS bindings (
|
||||
user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
desktop_id text NOT NULL REFERENCES desktops(id), PRIMARY KEY(user_id,desktop_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS resources (
|
||||
id text PRIMARY KEY, name text NOT NULL, status text NOT NULL, cpu double precision NOT NULL,
|
||||
memory double precision NOT NULL, storage double precision NOT NULL, provider text NOT NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id text PRIMARY KEY, kind text NOT NULL, action text NOT NULL, target_id text NOT NULL, target_name text NOT NULL,
|
||||
actor text NOT NULL, status text NOT NULL, message text NOT NULL DEFAULT '', provider_ref text NOT NULL DEFAULT '',
|
||||
idempotency_key text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(actor,idempotency_key)
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS tasks_active_target ON tasks(kind,target_id) WHERE status IN ('queued','running');
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id text PRIMARY KEY, actor text NOT NULL, action text NOT NULL, target text NOT NULL, result text NOT NULL,
|
||||
request_id text NOT NULL, created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS connection_tickets (
|
||||
token_hash text PRIMARY KEY, terminal_id text NOT NULL REFERENCES terminals(id), desktop_id text NOT NULL REFERENCES desktops(id),
|
||||
expires_at timestamptz NOT NULL, consumed_at timestamptz
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS audit_created ON audit_logs(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS tasks_created ON tasks(created_at DESC);
|
||||
ALTER TABLE terminals ADD COLUMN IF NOT EXISTS session_id text NOT NULL DEFAULT '';
|
||||
ALTER TABLE terminals ADD COLUMN IF NOT EXISTS session_state text NOT NULL DEFAULT 'idle';
|
||||
CREATE TABLE IF NOT EXISTS task_diagnostics (
|
||||
task_id text PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
content jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
@@ -0,0 +1,119 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"time"
|
||||
"vdi-cloud/platform/internal/model"
|
||||
)
|
||||
|
||||
//go:embed schema.sql
|
||||
var schema string
|
||||
|
||||
func Open(ctx context.Context, url string) (*sqlx.DB, error) {
|
||||
db, err := sqlx.ConnectContext(ctx, "pgx", url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db.SetMaxOpenConns(12)
|
||||
db.SetMaxIdleConns(4)
|
||||
db.SetConnMaxLifetime(30 * time.Minute)
|
||||
if _, err = db.ExecContext(ctx, schema); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
func ID(prefix string) string {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return prefix + hex.EncodeToString(b)
|
||||
}
|
||||
func Hash(secret string) string { h := sha256.Sum256([]byte(secret)); return hex.EncodeToString(h[:]) }
|
||||
func Bootstrap(ctx context.Context, db *sqlx.DB, mode, username, password string) error {
|
||||
if _, err := db.ExecContext(ctx, "INSERT INTO platform_metadata(singleton,mode) VALUES(true,$1) ON CONFLICT DO NOTHING", mode); err != nil {
|
||||
return err
|
||||
}
|
||||
var existingMode string
|
||||
if err := db.GetContext(ctx, &existingMode, "SELECT mode FROM platform_metadata WHERE singleton=true"); err != nil {
|
||||
return err
|
||||
}
|
||||
if existingMode != mode {
|
||||
return fmt.Errorf("database mode is %s; use a separate database for %s", existingMode, mode)
|
||||
}
|
||||
admins := []struct{ username, name, role, password string }{{username, "平台管理员", "super_admin", password}}
|
||||
if mode == "demo" {
|
||||
admins = []struct{ username, name, role, password string }{{"admin", "系统管理员", "super_admin", "Demo@2026"}, {"operator", "运维人员", "operator", "Demo@2026"}, {"viewer", "只读人员", "viewer", "Demo@2026"}}
|
||||
}
|
||||
for _, a := range admins {
|
||||
var exists bool
|
||||
if err := db.GetContext(ctx, &exists, "SELECT EXISTS(SELECT 1 FROM administrators WHERE username=$1)", a.username); err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
if mode == "production" && len(a.password) < 12 {
|
||||
return fmt.Errorf("initial administrator password must contain at least 12 characters")
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(a.password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = db.ExecContext(ctx, "INSERT INTO administrators VALUES($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING", ID("adm_"), a.username, a.name, a.role, string(hash)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if mode != "demo" {
|
||||
return nil
|
||||
}
|
||||
// Seed the demo only once; restarting must not restore revoked bindings.
|
||||
var seeded bool
|
||||
if err := db.GetContext(ctx, &seeded, "SELECT EXISTS(SELECT 1 FROM desktops WHERE provider='demo')"); err != nil {
|
||||
return err
|
||||
}
|
||||
if seeded {
|
||||
return nil
|
||||
}
|
||||
tx, err := db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
statements := []string{
|
||||
`INSERT INTO desktops(id,name,os,node,vmid,pool,power_state,service_state,provider) VALUES ('desk_demo_windows','设计专属桌面','Windows','demo-node-a',201,'设计专属池','running','unknown','demo'),('desk_demo_ubuntu','研发办公桌面','Ubuntu','demo-node-a',202,'研发专属池','running','unknown','demo'),('desk_demo_kylin','国产办公桌面','麒麟','demo-node-a',203,'通用办公池','stopped','unknown','demo') ON CONFLICT DO NOTHING`,
|
||||
`INSERT INTO terminals(id,sn,name,group_name,status,agent_version,last_seen,bootstrap_hash) VALUES ('term_demo_001','DEMO-RK-001','设计工位 01','设计部','offline','演示版本',now()-interval '2 hours','disabled'),('term_demo_002','DEMO-RK-002','研发工位 02','研发部','offline','演示版本',now()-interval '3 hours','disabled') ON CONFLICT DO NOTHING`,
|
||||
`INSERT INTO users(id,username,display_name,terminal_id) VALUES ('user_demo_wang','wang.demo','王工','term_demo_001'),('user_demo_chen','chen.demo','陈工','term_demo_002') ON CONFLICT DO NOTHING`,
|
||||
`INSERT INTO bindings VALUES ('user_demo_wang','desk_demo_windows'),('user_demo_wang','desk_demo_ubuntu'),('user_demo_chen','desk_demo_ubuntu') ON CONFLICT DO NOTHING`,
|
||||
`INSERT INTO resources VALUES ('demo-node-a','demo-node-a','online',0.24,0.48,0.35,'demo',now()) ON CONFLICT DO NOTHING`,
|
||||
}
|
||||
for _, q := range statements {
|
||||
if _, err = tx.ExecContext(ctx, q); err != nil {
|
||||
return fmt.Errorf("demo seed: %w", err)
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func Users(ctx context.Context, db *sqlx.DB) ([]model.User, error) {
|
||||
users := []model.User{}
|
||||
if err := db.SelectContext(ctx, &users, "SELECT * FROM users ORDER BY username"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range users {
|
||||
users[i].DesktopIDs = []string{}
|
||||
if err := db.SelectContext(ctx, &users[i].DesktopIDs, "SELECT desktop_id FROM bindings WHERE user_id=$1 ORDER BY desktop_id", users[i].ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
Reference in new issue
Block a user