@@ -2,6 +2,7 @@ node_modules
|
||||
dist
|
||||
build
|
||||
.env
|
||||
.env.bck
|
||||
docs/*
|
||||
!docs/superpowers/
|
||||
.superpowers
|
||||
|
||||
@@ -7,7 +7,16 @@ import { Field } from "@/components/Field";
|
||||
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
export function LinkForm({ onLinked }: { onLinked: (instanceId: string) => void }) {
|
||||
export function LinkForm({
|
||||
onLinked,
|
||||
claimId,
|
||||
}: {
|
||||
onLinked: (instanceId: string) => void;
|
||||
// When set, this is a PAID placeholder awaiting its real install UUID: claim
|
||||
// it in place rather than creating a fresh (Free) instance. The name was
|
||||
// chosen at checkout, so it is not asked for again.
|
||||
claimId?: string;
|
||||
}) {
|
||||
const [id, setId] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
@@ -28,7 +37,9 @@ export function LinkForm({ onLinked }: { onLinked: (instanceId: string) => void
|
||||
setBusy(true);
|
||||
setError(undefined);
|
||||
try {
|
||||
const inst = await api.link(value, name.trim());
|
||||
const inst = claimId
|
||||
? await api.claimLink(claimId, value)
|
||||
: await api.link(value, name.trim());
|
||||
onLinked(inst.instance_id);
|
||||
} catch (err) {
|
||||
setError(
|
||||
@@ -58,12 +69,14 @@ export function LinkForm({ onLinked }: { onLinked: (instanceId: string) => void
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Field
|
||||
label="Name it (optional)"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
hint="So you can tell it apart from your other installs."
|
||||
/>
|
||||
{!claimId && (
|
||||
<Field
|
||||
label="Name it (optional)"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
hint="So you can tell it apart from your other installs."
|
||||
/>
|
||||
)}
|
||||
<Button type="submit" disabled={busy} className="justify-self-start">
|
||||
{busy ? "Linking…" : "Link and issue licence"}
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { LinkForm } from "./LinkForm";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
@@ -8,6 +8,9 @@ import { PageHeader } from "@/components/PageHeader";
|
||||
export default function LinkPage() {
|
||||
const router = useRouter();
|
||||
const qc = useQueryClient();
|
||||
// A paid placeholder passes its id here so its install is claimed in place
|
||||
// rather than a second, Free instance being created alongside it.
|
||||
const claimId = useSearchParams().get("claim") ?? undefined;
|
||||
|
||||
return (
|
||||
<div className="grid max-w-2xl gap-6">
|
||||
@@ -17,6 +20,7 @@ export default function LinkPage() {
|
||||
subtitle="Every licence is tied to one install, so we need its ID before we can issue yours. Paste it below and your licence is ready on the next screen."
|
||||
/>
|
||||
<LinkForm
|
||||
claimId={claimId}
|
||||
onLinked={(instanceId) => {
|
||||
qc.invalidateQueries({ queryKey: ["account"] });
|
||||
// Straight to the download, not back to a list: the licence is
|
||||
|
||||
@@ -221,7 +221,17 @@ export function InstanceRecord({
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
{state === "none" ? (
|
||||
<LinkButton href="/instances/link">Link an install</LinkButton>
|
||||
// A paid placeholder (awaiting_link) must CLAIM its install, not
|
||||
// create a second Free instance beside it — so pass its id.
|
||||
<LinkButton
|
||||
href={
|
||||
instance.status === "awaiting_link"
|
||||
? `/instances/link?claim=${instance.instance_id}`
|
||||
: "/instances/link"
|
||||
}
|
||||
>
|
||||
Link an install
|
||||
</LinkButton>
|
||||
) : cloud && instance.slug ? (
|
||||
<>
|
||||
<LinkButton
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Vantage self-hosted — copy to .env and fill in.
|
||||
# Used by: docker compose up -d
|
||||
|
||||
# --- Required ---
|
||||
|
||||
# host:port agents dial for gRPC. No default; boot fails without it.
|
||||
# Must be reachable from managed servers. Use the public host, port 9090.
|
||||
GRPC_HOST=192.168.1.250:9090
|
||||
|
||||
# Gitea host used to build agent install scripts and download URLs.
|
||||
GITEA_HOST=gitea.hostxtra.co.uk
|
||||
|
||||
# 64-char hex (32 bytes) for AES-256-GCM. Required for private keys,
|
||||
# secrets, OIDC secrets, RDP/VNC credentials.
|
||||
# Generate: openssl rand -hex 32
|
||||
KEY_ENCRYPTION_KEY=
|
||||
|
||||
# --- Optional (defaults shown) ---
|
||||
|
||||
# MongoDB is bundled in this compose file. Override only to use an external DB.
|
||||
MONGO_URI=mongodb://mongo:27017/vantage
|
||||
|
||||
# Where workflow run logs are written inside the server container.
|
||||
# VANTAGE_WORKFLOW_LOG_DIR=/data/workflow-logs
|
||||
@@ -12,6 +12,21 @@ services:
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
mongo:
|
||||
image: mongo:7
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- mongo_data:/data/db
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- mongosh
|
||||
- --quiet
|
||||
- --eval
|
||||
- "db.adminCommand('ping')"
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
guacd:
|
||||
image: docker.io/guacamole/guacd:1.6.0
|
||||
restart: unless-stopped
|
||||
@@ -24,7 +39,7 @@ services:
|
||||
- 8080:8080
|
||||
- 9090:9090
|
||||
environment:
|
||||
MONGO_URI: ${MONGO_URI:-}
|
||||
MONGO_URI: ${MONGO_URI:-mongodb://mongo:27017/vantage}
|
||||
REDIS_ADDR: redis:6379
|
||||
GITEA_HOST: ${GITEA_HOST}
|
||||
GRPC_HOST: ${GRPC_HOST}
|
||||
@@ -37,6 +52,8 @@ services:
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
mongo:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./data:/data
|
||||
web:
|
||||
|
||||
+2
-2
@@ -17,13 +17,13 @@ import (
|
||||
|
||||
func main() {
|
||||
mongoURI := getEnv("MONGO_URI", "mongodb://localhost:27017")
|
||||
dbName := getEnv("MONGO_DB", "vantage")
|
||||
|
||||
if os.Getenv("GRPC_HOST") == "" {
|
||||
log.Fatal("GRPC_HOST is required (host:port agents dial for gRPC)")
|
||||
}
|
||||
|
||||
if err := db.Connect(mongoURI, dbName); err != nil {
|
||||
// DB name comes from the MONGO_URI path; "vantage" is the fallback.
|
||||
if err := db.Connect(mongoURI, "vantage"); err != nil {
|
||||
log.Fatalf("failed to connect to MongoDB: %v", err)
|
||||
}
|
||||
log.Println("connected to MongoDB")
|
||||
|
||||
@@ -2,16 +2,30 @@ package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/v2/x/mongo/driver/connstring"
|
||||
)
|
||||
|
||||
var Client *mongo.Client
|
||||
var Database *mongo.Database
|
||||
|
||||
func Connect(uri, dbName string) error {
|
||||
// Connect resolves the database name from the URI path (e.g.
|
||||
// mongodb://host:27017/vantage), falling back to fallbackDB when the URI names
|
||||
// none, so a bare URI still works without a separate MONGO_DB env var.
|
||||
func Connect(uri, fallbackDB string) error {
|
||||
cs, err := connstring.ParseAndValidate(uri)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse MONGO_URI: %w", err)
|
||||
}
|
||||
dbName := cs.Database
|
||||
if dbName == "" {
|
||||
dbName = fallbackDB
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user