diff --git a/apps/dashboard/drizzle/0024_curious_scourge.sql b/apps/dashboard/drizzle/0024_curious_scourge.sql new file mode 100644 index 0000000..36971a3 --- /dev/null +++ b/apps/dashboard/drizzle/0024_curious_scourge.sql @@ -0,0 +1 @@ +ALTER TABLE "organization" ADD COLUMN "billing_exempt" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/apps/dashboard/drizzle/0025_charming_roland_deschain.sql b/apps/dashboard/drizzle/0025_charming_roland_deschain.sql new file mode 100644 index 0000000..b6e8f92 --- /dev/null +++ b/apps/dashboard/drizzle/0025_charming_roland_deschain.sql @@ -0,0 +1,2 @@ +ALTER TABLE "billing_usage_events" ADD COLUMN "note" text;--> statement-breakpoint +ALTER TABLE "vms" ADD COLUMN "deleted_at" bigint; \ No newline at end of file diff --git a/apps/dashboard/drizzle/0026_lively_talos.sql b/apps/dashboard/drizzle/0026_lively_talos.sql new file mode 100644 index 0000000..dd4e8b2 --- /dev/null +++ b/apps/dashboard/drizzle/0026_lively_talos.sql @@ -0,0 +1 @@ +ALTER TABLE "organization" ADD COLUMN "disabled" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/apps/dashboard/drizzle/meta/0024_snapshot.json b/apps/dashboard/drizzle/meta/0024_snapshot.json new file mode 100644 index 0000000..6c8caa0 --- /dev/null +++ b/apps/dashboard/drizzle/meta/0024_snapshot.json @@ -0,0 +1,2534 @@ +{ + "id": "3341e0f0-3bfe-4024-a4b2-6b0165fe00dd", + "prevId": "9195c42d-def5-4e29-9352-335947f4295b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_tokens": { + "name": "api_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "api_tokens_user_id_index": { + "name": "api_tokens_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.base_images": { + "name": "base_images", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'bg-gray-600'" + }, + "is_official": { + "name": "is_official", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "logo_svg": { + "name": "logo_svg", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#6b7280'" + }, + "image_type": { + "name": "image_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'qcow2'" + }, + "secure_boot": { + "name": "secure_boot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "isa": { + "name": "isa", + "type": "vm_isa", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_meters": { + "name": "billing_meters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "billing_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "units": { + "name": "units", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_metered_at": { + "name": "last_metered_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "billing_meters_resource_index": { + "name": "billing_meters_resource_index", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_meters_active_last_metered_at_index": { + "name": "billing_meters_active_last_metered_at_index", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_metered_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_meters_project_active_index": { + "name": "billing_meters_project_active_index", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_usage_events": { + "name": "billing_usage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "billing_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_status": { + "name": "sync_status", + "type": "billing_sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "billing_usage_events_idempotency_key_index": { + "name": "billing_usage_events_idempotency_key_index", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_usage_events_project_created_at_index": { + "name": "billing_usage_events_project_created_at_index", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_usage_events_sync_status_created_at_index": { + "name": "billing_usage_events_sync_status_created_at_index", + "columns": [ + { + "expression": "sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_usage_events_resource_index": { + "name": "billing_usage_events_resource_index", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ip_assignments": { + "name": "ip_assignments", + "schema": "", + "columns": { + "ip": { + "name": "ip", + "type": "inet", + "primaryKey": false, + "notNull": true + }, + "ip_block_id": { + "name": "ip_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ip_assignments_ip_block_id_index": { + "name": "ip_assignments_ip_block_id_index", + "columns": [ + { + "expression": "ip_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ip_assignments_associated_vm_id_index": { + "name": "ip_assignments_associated_vm_id_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ip_assignments_ip_block_id_ip_blocks_id_fk": { + "name": "ip_assignments_ip_block_id_ip_blocks_id_fk", + "tableFrom": "ip_assignments", + "tableTo": "ip_blocks", + "columnsFrom": [ + "ip_block_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ip_assignments_associated_vm_id_vms_id_fk": { + "name": "ip_assignments_associated_vm_id_vms_id_fk", + "tableFrom": "ip_assignments", + "tableTo": "vms", + "columnsFrom": [ + "associated_vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ip_blocks": { + "name": "ip_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ip_block": { + "name": "ip_block", + "type": "cidr", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ipam_allocations": { + "name": "ipam_allocations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ipam_prefix_id": { + "name": "ipam_prefix_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "family": { + "name": "family", + "type": "ip_family", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "cidr", + "primaryKey": false, + "notNull": false + }, + "prefix_length": { + "name": "prefix_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "ipam_allocations_ipam_prefix_id_index": { + "name": "ipam_allocations_ipam_prefix_id_index", + "columns": [ + { + "expression": "ipam_prefix_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_associated_vm_id_index": { + "name": "ipam_allocations_associated_vm_id_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_vm_family_index": { + "name": "ipam_allocations_vm_family_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "family", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_address_index": { + "name": "ipam_allocations_address_index", + "columns": [ + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_prefix_index": { + "name": "ipam_allocations_prefix_index", + "columns": [ + { + "expression": "prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ipam_allocations_ipam_prefix_id_ipam_prefixes_id_fk": { + "name": "ipam_allocations_ipam_prefix_id_ipam_prefixes_id_fk", + "tableFrom": "ipam_allocations", + "tableTo": "ipam_prefixes", + "columnsFrom": [ + "ipam_prefix_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ipam_allocations_associated_vm_id_vms_id_fk": { + "name": "ipam_allocations_associated_vm_id_vms_id_fk", + "tableFrom": "ipam_allocations", + "tableTo": "vms", + "columnsFrom": [ + "associated_vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ipam_prefixes": { + "name": "ipam_prefixes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cidr": { + "name": "cidr", + "type": "cidr", + "primaryKey": false, + "notNull": true + }, + "family": { + "name": "family", + "type": "ip_family", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ipv6_use_transit_address": { + "name": "ipv6_use_transit_address", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "whitelist_start": { + "name": "whitelist_start", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "whitelist_end": { + "name": "whitelist_end", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "gateway_address": { + "name": "gateway_address", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "ipam_prefixes_cidr_index": { + "name": "ipam_prefixes_cidr_index", + "columns": [ + { + "expression": "cidr", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_prefixes_family_disabled_index": { + "name": "ipam_prefixes_family_disabled_index", + "columns": [ + { + "expression": "family", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "disabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_periods": { + "name": "payment_periods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "vm_id": { + "name": "vm_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate": { + "name": "rate", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "cap": { + "name": "cap", + "type": "numeric", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "payment_periods_vm_id_index": { + "name": "payment_periods_vm_id_index", + "columns": [ + { + "expression": "vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payment_periods_user_id_index": { + "name": "payment_periods_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payment_periods_vm_id_vms_id_fk": { + "name": "payment_periods_vm_id_vms_id_fk", + "tableFrom": "payment_periods", + "tableTo": "vms", + "columnsFrom": [ + "vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_billing_customers": { + "name": "project_billing_customers", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "autumn_customer_id": { + "name": "autumn_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_status": { + "name": "sync_status", + "type": "billing_sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "past_due_since": { + "name": "past_due_since", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "project_billing_customers_autumn_customer_id_index": { + "name": "project_billing_customers_autumn_customer_id_index", + "columns": [ + { + "expression": "autumn_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_keys": { + "name": "ssh_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ssh_keys_user_id_index": { + "name": "ssh_keys_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vm_types": { + "name": "vm_types", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isa": { + "name": "isa", + "type": "vm_isa", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cores": { + "name": "cores", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ram_capacity": { + "name": "ram_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "storage_amount": { + "name": "storage_amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rate": { + "name": "rate", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "cap": { + "name": "cap", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "autumn_feature_id": { + "name": "autumn_feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vms": { + "name": "vms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proxmox_id": { + "name": "proxmox_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "proxmox_node": { + "name": "proxmox_node", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_known_ipv4": { + "name": "last_known_ipv4", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "last_known_ipv6": { + "name": "last_known_ipv6", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "last_known_status": { + "name": "last_known_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_known_uptime": { + "name": "last_known_uptime", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_known_at": { + "name": "last_known_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "owner_project_id": { + "name": "owner_project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vm_type_id": { + "name": "vm_type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creation_date": { + "name": "creation_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + }, + "backend": { + "name": "backend", + "type": "vm_backend", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "vm_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'provisioning'" + }, + "status_error": { + "name": "status_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "vms_owner_project_id_index": { + "name": "vms_owner_project_id_index", + "columns": [ + { + "expression": "owner_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vms_owner_project_active_index": { + "name": "vms_owner_project_active_index", + "columns": [ + { + "expression": "owner_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vms_proxmox_id_index": { + "name": "vms_proxmox_id_index", + "columns": [ + { + "expression": "proxmox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vms_owner_project_id_organization_id_fk": { + "name": "vms_owner_project_id_organization_id_fk", + "tableFrom": "vms", + "tableTo": "organization", + "columnsFrom": [ + "owner_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vms_vm_type_id_vm_types_id_fk": { + "name": "vms_vm_type_id_vm_types_id_fk", + "tableFrom": "vms", + "tableTo": "vm_types", + "columnsFrom": [ + "vm_type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volumes": { + "name": "volumes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_project_id": { + "name": "owner_project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "volumes_owner_project_id_index": { + "name": "volumes_owner_project_id_index", + "columns": [ + { + "expression": "owner_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "volumes_associated_vm_id_index": { + "name": "volumes_associated_vm_id_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "volumes_owner_project_id_organization_id_fk": { + "name": "volumes_owner_project_id_organization_id_fk", + "tableFrom": "volumes", + "tableTo": "organization", + "columnsFrom": [ + "owner_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "volumes_associated_vm_id_vms_id_fk": { + "name": "volumes_associated_vm_id_vms_id_fk", + "tableFrom": "volumes", + "tableTo": "vms", + "columnsFrom": [ + "associated_vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_exempt": { + "name": "billing_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialID_idx": { + "name": "passkey_credentialID_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "twoFactor_secret_idx": { + "name": "twoFactor_secret_idx", + "columns": [ + { + "expression": "secret", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "twoFactor_userId_idx": { + "name": "twoFactor_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_exempt": { + "name": "billing_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.billing_resource_type": { + "name": "billing_resource_type", + "schema": "public", + "values": [ + "vm", + "volume" + ] + }, + "public.billing_sync_status": { + "name": "billing_sync_status", + "schema": "public", + "values": [ + "pending", + "synced", + "failed" + ] + }, + "public.ip_family": { + "name": "ip_family", + "schema": "public", + "values": [ + "ipv4", + "ipv6" + ] + }, + "public.vm_backend": { + "name": "vm_backend", + "schema": "public", + "values": [ + "proxmox" + ] + }, + "public.vm_isa": { + "name": "vm_isa", + "schema": "public", + "values": [ + "x86", + "arm", + "risc-v" + ] + }, + "public.vm_status": { + "name": "vm_status", + "schema": "public", + "values": [ + "provisioning", + "ready", + "error", + "deleting" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dashboard/drizzle/meta/0025_snapshot.json b/apps/dashboard/drizzle/meta/0025_snapshot.json new file mode 100644 index 0000000..9f0d59d --- /dev/null +++ b/apps/dashboard/drizzle/meta/0025_snapshot.json @@ -0,0 +1,2546 @@ +{ + "id": "5c512083-873c-4237-baf7-4f809d6b8801", + "prevId": "3341e0f0-3bfe-4024-a4b2-6b0165fe00dd", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_tokens": { + "name": "api_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "api_tokens_user_id_index": { + "name": "api_tokens_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.base_images": { + "name": "base_images", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'bg-gray-600'" + }, + "is_official": { + "name": "is_official", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "logo_svg": { + "name": "logo_svg", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#6b7280'" + }, + "image_type": { + "name": "image_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'qcow2'" + }, + "secure_boot": { + "name": "secure_boot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "isa": { + "name": "isa", + "type": "vm_isa", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_meters": { + "name": "billing_meters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "billing_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "units": { + "name": "units", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_metered_at": { + "name": "last_metered_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "billing_meters_resource_index": { + "name": "billing_meters_resource_index", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_meters_active_last_metered_at_index": { + "name": "billing_meters_active_last_metered_at_index", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_metered_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_meters_project_active_index": { + "name": "billing_meters_project_active_index", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_usage_events": { + "name": "billing_usage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "billing_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "billing_sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "billing_usage_events_idempotency_key_index": { + "name": "billing_usage_events_idempotency_key_index", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_usage_events_project_created_at_index": { + "name": "billing_usage_events_project_created_at_index", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_usage_events_sync_status_created_at_index": { + "name": "billing_usage_events_sync_status_created_at_index", + "columns": [ + { + "expression": "sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_usage_events_resource_index": { + "name": "billing_usage_events_resource_index", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ip_assignments": { + "name": "ip_assignments", + "schema": "", + "columns": { + "ip": { + "name": "ip", + "type": "inet", + "primaryKey": false, + "notNull": true + }, + "ip_block_id": { + "name": "ip_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ip_assignments_ip_block_id_index": { + "name": "ip_assignments_ip_block_id_index", + "columns": [ + { + "expression": "ip_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ip_assignments_associated_vm_id_index": { + "name": "ip_assignments_associated_vm_id_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ip_assignments_ip_block_id_ip_blocks_id_fk": { + "name": "ip_assignments_ip_block_id_ip_blocks_id_fk", + "tableFrom": "ip_assignments", + "tableTo": "ip_blocks", + "columnsFrom": [ + "ip_block_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ip_assignments_associated_vm_id_vms_id_fk": { + "name": "ip_assignments_associated_vm_id_vms_id_fk", + "tableFrom": "ip_assignments", + "tableTo": "vms", + "columnsFrom": [ + "associated_vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ip_blocks": { + "name": "ip_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ip_block": { + "name": "ip_block", + "type": "cidr", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ipam_allocations": { + "name": "ipam_allocations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ipam_prefix_id": { + "name": "ipam_prefix_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "family": { + "name": "family", + "type": "ip_family", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "cidr", + "primaryKey": false, + "notNull": false + }, + "prefix_length": { + "name": "prefix_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "ipam_allocations_ipam_prefix_id_index": { + "name": "ipam_allocations_ipam_prefix_id_index", + "columns": [ + { + "expression": "ipam_prefix_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_associated_vm_id_index": { + "name": "ipam_allocations_associated_vm_id_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_vm_family_index": { + "name": "ipam_allocations_vm_family_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "family", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_address_index": { + "name": "ipam_allocations_address_index", + "columns": [ + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_prefix_index": { + "name": "ipam_allocations_prefix_index", + "columns": [ + { + "expression": "prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ipam_allocations_ipam_prefix_id_ipam_prefixes_id_fk": { + "name": "ipam_allocations_ipam_prefix_id_ipam_prefixes_id_fk", + "tableFrom": "ipam_allocations", + "tableTo": "ipam_prefixes", + "columnsFrom": [ + "ipam_prefix_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ipam_allocations_associated_vm_id_vms_id_fk": { + "name": "ipam_allocations_associated_vm_id_vms_id_fk", + "tableFrom": "ipam_allocations", + "tableTo": "vms", + "columnsFrom": [ + "associated_vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ipam_prefixes": { + "name": "ipam_prefixes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cidr": { + "name": "cidr", + "type": "cidr", + "primaryKey": false, + "notNull": true + }, + "family": { + "name": "family", + "type": "ip_family", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ipv6_use_transit_address": { + "name": "ipv6_use_transit_address", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "whitelist_start": { + "name": "whitelist_start", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "whitelist_end": { + "name": "whitelist_end", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "gateway_address": { + "name": "gateway_address", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "ipam_prefixes_cidr_index": { + "name": "ipam_prefixes_cidr_index", + "columns": [ + { + "expression": "cidr", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_prefixes_family_disabled_index": { + "name": "ipam_prefixes_family_disabled_index", + "columns": [ + { + "expression": "family", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "disabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_periods": { + "name": "payment_periods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "vm_id": { + "name": "vm_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate": { + "name": "rate", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "cap": { + "name": "cap", + "type": "numeric", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "payment_periods_vm_id_index": { + "name": "payment_periods_vm_id_index", + "columns": [ + { + "expression": "vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payment_periods_user_id_index": { + "name": "payment_periods_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payment_periods_vm_id_vms_id_fk": { + "name": "payment_periods_vm_id_vms_id_fk", + "tableFrom": "payment_periods", + "tableTo": "vms", + "columnsFrom": [ + "vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_billing_customers": { + "name": "project_billing_customers", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "autumn_customer_id": { + "name": "autumn_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_status": { + "name": "sync_status", + "type": "billing_sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "past_due_since": { + "name": "past_due_since", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "project_billing_customers_autumn_customer_id_index": { + "name": "project_billing_customers_autumn_customer_id_index", + "columns": [ + { + "expression": "autumn_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_keys": { + "name": "ssh_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ssh_keys_user_id_index": { + "name": "ssh_keys_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vm_types": { + "name": "vm_types", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isa": { + "name": "isa", + "type": "vm_isa", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cores": { + "name": "cores", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ram_capacity": { + "name": "ram_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "storage_amount": { + "name": "storage_amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rate": { + "name": "rate", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "cap": { + "name": "cap", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "autumn_feature_id": { + "name": "autumn_feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vms": { + "name": "vms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proxmox_id": { + "name": "proxmox_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "proxmox_node": { + "name": "proxmox_node", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_known_ipv4": { + "name": "last_known_ipv4", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "last_known_ipv6": { + "name": "last_known_ipv6", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "last_known_status": { + "name": "last_known_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_known_uptime": { + "name": "last_known_uptime", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_known_at": { + "name": "last_known_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "owner_project_id": { + "name": "owner_project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vm_type_id": { + "name": "vm_type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creation_date": { + "name": "creation_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + }, + "backend": { + "name": "backend", + "type": "vm_backend", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "vm_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'provisioning'" + }, + "status_error": { + "name": "status_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "vms_owner_project_id_index": { + "name": "vms_owner_project_id_index", + "columns": [ + { + "expression": "owner_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vms_owner_project_active_index": { + "name": "vms_owner_project_active_index", + "columns": [ + { + "expression": "owner_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vms_proxmox_id_index": { + "name": "vms_proxmox_id_index", + "columns": [ + { + "expression": "proxmox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vms_owner_project_id_organization_id_fk": { + "name": "vms_owner_project_id_organization_id_fk", + "tableFrom": "vms", + "tableTo": "organization", + "columnsFrom": [ + "owner_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vms_vm_type_id_vm_types_id_fk": { + "name": "vms_vm_type_id_vm_types_id_fk", + "tableFrom": "vms", + "tableTo": "vm_types", + "columnsFrom": [ + "vm_type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volumes": { + "name": "volumes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_project_id": { + "name": "owner_project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "volumes_owner_project_id_index": { + "name": "volumes_owner_project_id_index", + "columns": [ + { + "expression": "owner_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "volumes_associated_vm_id_index": { + "name": "volumes_associated_vm_id_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "volumes_owner_project_id_organization_id_fk": { + "name": "volumes_owner_project_id_organization_id_fk", + "tableFrom": "volumes", + "tableTo": "organization", + "columnsFrom": [ + "owner_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "volumes_associated_vm_id_vms_id_fk": { + "name": "volumes_associated_vm_id_vms_id_fk", + "tableFrom": "volumes", + "tableTo": "vms", + "columnsFrom": [ + "associated_vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_exempt": { + "name": "billing_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialID_idx": { + "name": "passkey_credentialID_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "twoFactor_secret_idx": { + "name": "twoFactor_secret_idx", + "columns": [ + { + "expression": "secret", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "twoFactor_userId_idx": { + "name": "twoFactor_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_exempt": { + "name": "billing_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.billing_resource_type": { + "name": "billing_resource_type", + "schema": "public", + "values": [ + "vm", + "volume" + ] + }, + "public.billing_sync_status": { + "name": "billing_sync_status", + "schema": "public", + "values": [ + "pending", + "synced", + "failed" + ] + }, + "public.ip_family": { + "name": "ip_family", + "schema": "public", + "values": [ + "ipv4", + "ipv6" + ] + }, + "public.vm_backend": { + "name": "vm_backend", + "schema": "public", + "values": [ + "proxmox" + ] + }, + "public.vm_isa": { + "name": "vm_isa", + "schema": "public", + "values": [ + "x86", + "arm", + "risc-v" + ] + }, + "public.vm_status": { + "name": "vm_status", + "schema": "public", + "values": [ + "provisioning", + "ready", + "error", + "deleting" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dashboard/drizzle/meta/0026_snapshot.json b/apps/dashboard/drizzle/meta/0026_snapshot.json new file mode 100644 index 0000000..865ddaf --- /dev/null +++ b/apps/dashboard/drizzle/meta/0026_snapshot.json @@ -0,0 +1,2553 @@ +{ + "id": "e7c05c06-f886-45b0-ab2f-d7cb6382e31f", + "prevId": "5c512083-873c-4237-baf7-4f809d6b8801", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_tokens": { + "name": "api_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "api_tokens_user_id_index": { + "name": "api_tokens_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.base_images": { + "name": "base_images", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'bg-gray-600'" + }, + "is_official": { + "name": "is_official", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "logo_svg": { + "name": "logo_svg", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#6b7280'" + }, + "image_type": { + "name": "image_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'qcow2'" + }, + "secure_boot": { + "name": "secure_boot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "isa": { + "name": "isa", + "type": "vm_isa", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_meters": { + "name": "billing_meters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "billing_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "units": { + "name": "units", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_metered_at": { + "name": "last_metered_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "billing_meters_resource_index": { + "name": "billing_meters_resource_index", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_meters_active_last_metered_at_index": { + "name": "billing_meters_active_last_metered_at_index", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_metered_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_meters_project_active_index": { + "name": "billing_meters_project_active_index", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_usage_events": { + "name": "billing_usage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "billing_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "billing_sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "billing_usage_events_idempotency_key_index": { + "name": "billing_usage_events_idempotency_key_index", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_usage_events_project_created_at_index": { + "name": "billing_usage_events_project_created_at_index", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_usage_events_sync_status_created_at_index": { + "name": "billing_usage_events_sync_status_created_at_index", + "columns": [ + { + "expression": "sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_usage_events_resource_index": { + "name": "billing_usage_events_resource_index", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ip_assignments": { + "name": "ip_assignments", + "schema": "", + "columns": { + "ip": { + "name": "ip", + "type": "inet", + "primaryKey": false, + "notNull": true + }, + "ip_block_id": { + "name": "ip_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ip_assignments_ip_block_id_index": { + "name": "ip_assignments_ip_block_id_index", + "columns": [ + { + "expression": "ip_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ip_assignments_associated_vm_id_index": { + "name": "ip_assignments_associated_vm_id_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ip_assignments_ip_block_id_ip_blocks_id_fk": { + "name": "ip_assignments_ip_block_id_ip_blocks_id_fk", + "tableFrom": "ip_assignments", + "tableTo": "ip_blocks", + "columnsFrom": [ + "ip_block_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ip_assignments_associated_vm_id_vms_id_fk": { + "name": "ip_assignments_associated_vm_id_vms_id_fk", + "tableFrom": "ip_assignments", + "tableTo": "vms", + "columnsFrom": [ + "associated_vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ip_blocks": { + "name": "ip_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ip_block": { + "name": "ip_block", + "type": "cidr", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ipam_allocations": { + "name": "ipam_allocations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ipam_prefix_id": { + "name": "ipam_prefix_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "family": { + "name": "family", + "type": "ip_family", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "cidr", + "primaryKey": false, + "notNull": false + }, + "prefix_length": { + "name": "prefix_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "ipam_allocations_ipam_prefix_id_index": { + "name": "ipam_allocations_ipam_prefix_id_index", + "columns": [ + { + "expression": "ipam_prefix_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_associated_vm_id_index": { + "name": "ipam_allocations_associated_vm_id_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_vm_family_index": { + "name": "ipam_allocations_vm_family_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "family", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_address_index": { + "name": "ipam_allocations_address_index", + "columns": [ + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_prefix_index": { + "name": "ipam_allocations_prefix_index", + "columns": [ + { + "expression": "prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ipam_allocations_ipam_prefix_id_ipam_prefixes_id_fk": { + "name": "ipam_allocations_ipam_prefix_id_ipam_prefixes_id_fk", + "tableFrom": "ipam_allocations", + "tableTo": "ipam_prefixes", + "columnsFrom": [ + "ipam_prefix_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ipam_allocations_associated_vm_id_vms_id_fk": { + "name": "ipam_allocations_associated_vm_id_vms_id_fk", + "tableFrom": "ipam_allocations", + "tableTo": "vms", + "columnsFrom": [ + "associated_vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ipam_prefixes": { + "name": "ipam_prefixes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cidr": { + "name": "cidr", + "type": "cidr", + "primaryKey": false, + "notNull": true + }, + "family": { + "name": "family", + "type": "ip_family", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ipv6_use_transit_address": { + "name": "ipv6_use_transit_address", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "whitelist_start": { + "name": "whitelist_start", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "whitelist_end": { + "name": "whitelist_end", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "gateway_address": { + "name": "gateway_address", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "ipam_prefixes_cidr_index": { + "name": "ipam_prefixes_cidr_index", + "columns": [ + { + "expression": "cidr", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_prefixes_family_disabled_index": { + "name": "ipam_prefixes_family_disabled_index", + "columns": [ + { + "expression": "family", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "disabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_periods": { + "name": "payment_periods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "vm_id": { + "name": "vm_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate": { + "name": "rate", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "cap": { + "name": "cap", + "type": "numeric", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "payment_periods_vm_id_index": { + "name": "payment_periods_vm_id_index", + "columns": [ + { + "expression": "vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payment_periods_user_id_index": { + "name": "payment_periods_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payment_periods_vm_id_vms_id_fk": { + "name": "payment_periods_vm_id_vms_id_fk", + "tableFrom": "payment_periods", + "tableTo": "vms", + "columnsFrom": [ + "vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_billing_customers": { + "name": "project_billing_customers", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "autumn_customer_id": { + "name": "autumn_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_status": { + "name": "sync_status", + "type": "billing_sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "past_due_since": { + "name": "past_due_since", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "project_billing_customers_autumn_customer_id_index": { + "name": "project_billing_customers_autumn_customer_id_index", + "columns": [ + { + "expression": "autumn_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_keys": { + "name": "ssh_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ssh_keys_user_id_index": { + "name": "ssh_keys_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vm_types": { + "name": "vm_types", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isa": { + "name": "isa", + "type": "vm_isa", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cores": { + "name": "cores", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ram_capacity": { + "name": "ram_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "storage_amount": { + "name": "storage_amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rate": { + "name": "rate", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "cap": { + "name": "cap", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "autumn_feature_id": { + "name": "autumn_feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vms": { + "name": "vms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proxmox_id": { + "name": "proxmox_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "proxmox_node": { + "name": "proxmox_node", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_known_ipv4": { + "name": "last_known_ipv4", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "last_known_ipv6": { + "name": "last_known_ipv6", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "last_known_status": { + "name": "last_known_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_known_uptime": { + "name": "last_known_uptime", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_known_at": { + "name": "last_known_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "owner_project_id": { + "name": "owner_project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vm_type_id": { + "name": "vm_type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creation_date": { + "name": "creation_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + }, + "backend": { + "name": "backend", + "type": "vm_backend", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "vm_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'provisioning'" + }, + "status_error": { + "name": "status_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "vms_owner_project_id_index": { + "name": "vms_owner_project_id_index", + "columns": [ + { + "expression": "owner_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vms_owner_project_active_index": { + "name": "vms_owner_project_active_index", + "columns": [ + { + "expression": "owner_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vms_proxmox_id_index": { + "name": "vms_proxmox_id_index", + "columns": [ + { + "expression": "proxmox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vms_owner_project_id_organization_id_fk": { + "name": "vms_owner_project_id_organization_id_fk", + "tableFrom": "vms", + "tableTo": "organization", + "columnsFrom": [ + "owner_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vms_vm_type_id_vm_types_id_fk": { + "name": "vms_vm_type_id_vm_types_id_fk", + "tableFrom": "vms", + "tableTo": "vm_types", + "columnsFrom": [ + "vm_type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volumes": { + "name": "volumes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_project_id": { + "name": "owner_project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "volumes_owner_project_id_index": { + "name": "volumes_owner_project_id_index", + "columns": [ + { + "expression": "owner_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "volumes_associated_vm_id_index": { + "name": "volumes_associated_vm_id_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "volumes_owner_project_id_organization_id_fk": { + "name": "volumes_owner_project_id_organization_id_fk", + "tableFrom": "volumes", + "tableTo": "organization", + "columnsFrom": [ + "owner_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "volumes_associated_vm_id_vms_id_fk": { + "name": "volumes_associated_vm_id_vms_id_fk", + "tableFrom": "volumes", + "tableTo": "vms", + "columnsFrom": [ + "associated_vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_exempt": { + "name": "billing_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialID_idx": { + "name": "passkey_credentialID_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "twoFactor_secret_idx": { + "name": "twoFactor_secret_idx", + "columns": [ + { + "expression": "secret", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "twoFactor_userId_idx": { + "name": "twoFactor_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_exempt": { + "name": "billing_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.billing_resource_type": { + "name": "billing_resource_type", + "schema": "public", + "values": [ + "vm", + "volume" + ] + }, + "public.billing_sync_status": { + "name": "billing_sync_status", + "schema": "public", + "values": [ + "pending", + "synced", + "failed" + ] + }, + "public.ip_family": { + "name": "ip_family", + "schema": "public", + "values": [ + "ipv4", + "ipv6" + ] + }, + "public.vm_backend": { + "name": "vm_backend", + "schema": "public", + "values": [ + "proxmox" + ] + }, + "public.vm_isa": { + "name": "vm_isa", + "schema": "public", + "values": [ + "x86", + "arm", + "risc-v" + ] + }, + "public.vm_status": { + "name": "vm_status", + "schema": "public", + "values": [ + "provisioning", + "ready", + "error", + "deleting" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dashboard/drizzle/meta/_journal.json b/apps/dashboard/drizzle/meta/_journal.json index b7ea0e5..b112fb2 100644 --- a/apps/dashboard/drizzle/meta/_journal.json +++ b/apps/dashboard/drizzle/meta/_journal.json @@ -169,6 +169,27 @@ "when": 1783925195204, "tag": "0023_familiar_stranger", "breakpoints": true + }, + { + "idx": 24, + "version": "7", + "when": 1784298548259, + "tag": "0024_curious_scourge", + "breakpoints": true + }, + { + "idx": 25, + "version": "7", + "when": 1784301658548, + "tag": "0025_charming_roland_deschain", + "breakpoints": true + }, + { + "idx": 26, + "version": "7", + "when": 1784316474741, + "tag": "0026_lively_talos", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/dashboard/src/lib/components/admin/vm-billing-reversal-dialog.svelte b/apps/dashboard/src/lib/components/admin/vm-billing-reversal-dialog.svelte new file mode 100644 index 0000000..5ebf8cb --- /dev/null +++ b/apps/dashboard/src/lib/components/admin/vm-billing-reversal-dialog.svelte @@ -0,0 +1,242 @@ + + + + + + + Reverse billing for {vm?.name} + + + Credits back this server's metered usage in the selected window by recording a negative + usage event in {vm?.projectName ?? 'its project'} and syncing it to Autumn. + + + +
+
+
+ + +
+
+ + +
+
+ + {#if loadError} +
+ {loadError} +
+ {:else if !windowValid} +

Pick a valid window to compute billed usage.

+ {:else if loading} +
+ + Computing billed usage... +
+ {:else if usage} +
+
+ Billed in window + {formatHours(usage.billedHours)} unit-hours +
+
+ Already reversed + {formatHours(usage.reversedHours)} unit-hours +
+
+ To reverse + {formatHours(usage.reversibleHours)} unit-hours +
+ {#if usage.estimatedAmount != null} +
+ Estimated credit (before caps) + ${usage.estimatedAmount.toFixed(2)} +
+ {/if} +
+ {#if usage.reversibleHours <= 0} +

+ Nothing left to reverse in this window. Widen the dates to include more usage. +

+ {/if} + {/if} + +
+ + +
+
+ + + + + +
+
diff --git a/apps/dashboard/src/lib/components/admin/vm-detail-sheet.svelte b/apps/dashboard/src/lib/components/admin/vm-detail-sheet.svelte new file mode 100644 index 0000000..cb0f583 --- /dev/null +++ b/apps/dashboard/src/lib/components/admin/vm-detail-sheet.svelte @@ -0,0 +1,454 @@ + + + !value && onClose()}> + + {#if vm} + {@const info = statusInfo(vm)} + {@const saving = admin.adminVmSaving[vm.id]} +
+
+
+ {vm.name} + + {vm.lastKnownIpv4 ?? vm.lastKnownIpv6 ?? vm.id} + +
+ + {info.label} + +
+ + {#if vm.active} +
+ + + + +
+ {/if} + + {#if vm.statusError} +
+ + {vm.statusError} +
+ {/if} + +
+ Diagnostics +
+ + Live status + + {vm.liveStatus ?? '-'} +
+
+ + Uptime + + + {vm.liveStatus === 'running' ? formatUptime(vm.uptime) : '-'} + +
+ {#if vm.cpuUsage != null} +
+ + CPU + + {formatPercent(vm.cpuUsage)} +
+ {/if} + {#if vm.memoryUsageBytes != null && vm.memoryTotalBytes != null && vm.memoryTotalBytes > 0} +
+ + Memory + + + {formatGiB(vm.memoryUsageBytes)} / {formatGiB(vm.memoryTotalBytes)} + +
+ {/if} +
+ + Node + + {vm.proxmoxNode ?? '-'} +
+
+ + Proxmox VMID + + {vm.proxmoxId ?? '-'} +
+ {#if vm.lastKnownAt} +
+ + Last seen + + {formatDateTime(vm.lastKnownAt)} +
+ {/if} +
+ + IPv4 + + {vm.lastKnownIpv4 ?? '-'} +
+
+ + IPv6 + + + {vm.lastKnownIpv6 ?? '-'} + +
+
+ + + +
+ Type +
+ + Plan + + {vm.vmTypeName ?? '-'} +
+ {#if vm.vmTypeCores} +
+ + Resources + + + {vm.vmTypeCores}c · {vm.vmTypeRamCapacity} MB · {vm.vmTypeStorageAmount} GB + +
+ {/if} + {#if vm.vmTypeRate} +
+ + Rate + + ${vm.vmTypeRate}/hr +
+ {/if} +
+ + + +
+ Project +
+ + Project + + {#if vm.projectId} + + {vm.projectName ?? vm.projectId} + + {:else} + - + {/if} +
+
+ + Owner + + + {vm.ownerEmail ?? '-'} + {#if vm.ownerBillingExempt || vm.projectBillingExempt} + + No billing + + {/if} + +
+
+ + + +
+ Billing this month + {#if usageLoading} +
+ + Computing billed usage... +
+ {:else if usageError} +
+ {usageError} +
+ {:else if usage} +
+
+ Billed + {usage.billedHours.toFixed(2)} unit-hours +
+
+ Reversed + {usage.reversedHours.toFixed(2)} unit-hours +
+ {#if usage.estimatedAmount != null} +
+ Estimated charge (before caps) + ${usage.estimatedAmount.toFixed(2)} +
+ {/if} +
+ {/if} + +
+ + + +
+ Record +
+ + VM ID + + {vm.id} +
+
+ + Created + + {formatDateTime(vm.createdAt)} +
+ {#if vm.deletedAt} +
+ + Deleted + + {formatDateTime(vm.deletedAt)} +
+ {/if} +
+ + {#if vm.active} + + +
+
+ +
+ Delete server + + Deprovisions the server in Proxmox, releases its networking, and records final + usage. + +
+
+ +
+ {/if} +
+ {/if} +
+
diff --git a/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte b/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte index 7cc90bf..5843c37 100644 --- a/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte +++ b/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte @@ -906,7 +906,7 @@ class="mb-3 border border-amber-300 bg-amber-100 p-3 dark:border-amber-800/50 dark:bg-amber-950/20" >

- Copy this token now — it won't be shown again. + Copy this token now. It won't be shown again.

+ import { + Body, + Container, + Head, + Heading, + Hr, + Html, + Preview, + Section, + Text + } from '@better-svelte-email/components'; + import EmailHeader from './email-header.svelte'; + + interface Props { + userName?: string | null; + projectName: string; + code: string; + expiresInMinutes: number; + } + + let { userName = null, projectName, code, expiresInMinutes }: Props = $props(); + + + + + + + + +
+ + Confirm project deletion + + + {#if userName}Hi {userName},{:else}Hi there,{/if} + + + Enter this code in Stack to confirm deleting the project "{projectName}" and all of its + servers and volumes. + +
+ + {code} + +
+
+ + This code expires in {expiresInMinutes} minutes. If you did not request this deletion, contact + support@fyrastack.com and secure your account immediately. + +
+
+ + diff --git a/apps/dashboard/src/lib/emails/campaign-registry.ts b/apps/dashboard/src/lib/emails/campaign-registry.ts index e832bc8..8d8b628 100644 --- a/apps/dashboard/src/lib/emails/campaign-registry.ts +++ b/apps/dashboard/src/lib/emails/campaign-registry.ts @@ -7,6 +7,7 @@ export type CampaignField = { required: boolean; placeholder: string; defaultValue?: string; + options?: { value: string; label: string }[]; }; export type CampaignTemplate = { @@ -30,7 +31,7 @@ export const campaignTemplates: CampaignTemplate[] = [ { key: 'empty', label: 'Empty', - description: 'Blank template — just your text on the standard email shell.', + description: 'Blank template: just your text on the standard email shell.', defaultSubject: '', fields: [ { @@ -58,6 +59,84 @@ export const campaignTemplates: CampaignTemplate[] = [ } ] }, + { + key: 'service-disruption', + label: 'Service disruption', + description: 'Incident notice with a status banner, affected services, and status page link.', + defaultSubject: 'Service disruption: {{incident}}', + fields: [ + userNameField, + { + name: 'heading', + label: 'Incident title', + inline: true, + required: true, + placeholder: 'Incident title', + defaultValue: 'Degraded VM performance in our datacenter' + }, + { + name: 'preview', + label: 'Preview text', + inline: false, + required: true, + placeholder: 'Shown in inbox previews', + defaultValue: 'We are investigating a service disruption' + }, + { + name: 'severity', + label: 'Severity', + inline: false, + required: true, + placeholder: 'warning', + defaultValue: 'warning', + options: [ + { value: 'critical', label: 'Critical' }, + { value: 'warning', label: 'Warning' }, + { value: 'info', label: 'Info' }, + { value: 'resolved', label: 'Resolved' } + ] + }, + { + name: 'status', + label: 'Status', + inline: true, + required: true, + placeholder: 'Investigating / Identified / Monitoring / Resolved', + defaultValue: 'Investigating' + }, + { + name: 'affectedServices', + label: 'Affected services', + inline: true, + required: false, + placeholder: 'Affected services', + defaultValue: 'Virtual machines, volumes' + }, + { + name: 'timestamp', + label: 'Timestamp', + inline: true, + required: false, + placeholder: '2026-07-17 12:00 UTC' + }, + { + name: 'body', + label: 'Body', + inline: true, + required: true, + placeholder: 'What happened, impact, and next update', + defaultValue: + 'We are investigating an issue affecting some services. Your data is safe, and we will follow up here as soon as we know more.' + }, + { + name: 'statusUrl', + label: 'Status page URL', + inline: false, + required: false, + placeholder: 'https://status.fyrastack.com' + } + ] + }, { key: 'billing-notice', label: 'Billing notice', diff --git a/apps/dashboard/src/lib/emails/security-alert.svelte b/apps/dashboard/src/lib/emails/security-alert.svelte index 2b48cbe..78baafd 100644 --- a/apps/dashboard/src/lib/emails/security-alert.svelte +++ b/apps/dashboard/src/lib/emails/security-alert.svelte @@ -48,7 +48,7 @@ {message} {#if details} -
+
{details}
{/if} diff --git a/apps/dashboard/src/lib/emails/service-disruption.svelte b/apps/dashboard/src/lib/emails/service-disruption.svelte new file mode 100644 index 0000000..a066109 --- /dev/null +++ b/apps/dashboard/src/lib/emails/service-disruption.svelte @@ -0,0 +1,96 @@ + + + + + + + + +
+ {heading} +
+ + Status: {status} + + {#if affectedServices} + + Affected: {affectedServices} + + {/if} + {#if timestamp} + {timestamp} + {/if} +
+ + {#if userName}Hi {userName},{:else}Hi there,{/if} + + {body} + {#if statusUrl} +
+ +
+
+ + If the button above doesn't work, copy and paste this URL into your browser: + + + {statusUrl} + + {/if} +
+
+ + diff --git a/apps/dashboard/src/lib/remote/admin-billing.remote.ts b/apps/dashboard/src/lib/remote/admin-billing.remote.ts new file mode 100644 index 0000000..3def661 --- /dev/null +++ b/apps/dashboard/src/lib/remote/admin-billing.remote.ts @@ -0,0 +1,183 @@ +import { command, getRequestEvent, query } from '$app/server'; +import { error } from '@sveltejs/kit'; +import { type } from 'arktype'; +import { and, desc, eq, gt, lte } from 'drizzle-orm'; +import { requireAdmin } from '$lib/server/auth-context'; +import { initDrizzle } from '$lib/server/db'; +import { billingUsageEvents, organization, vms, vmTypes } from '$lib/server/db/schema'; +import { syncUsageEvent } from '$lib/server/billing/metering'; + +export type VmBillingUsage = { + vmId: string; + vmName: string; + projectId: string | null; + projectName: string | null; + featureId: string | null; + periodStart: number; + periodEnd: number; + billedHours: number; + reversedHours: number; + reversibleHours: number; + eventCount: number; + ratePerHour: string | null; + estimatedAmount: number | null; +}; + +async function requireCurrentAdmin() { + const event = getRequestEvent(); + if (!event?.locals.user) error(401, 'Authentication required'); + + const db = initDrizzle(); + await requireAdmin(db, event.locals.user.id); + + return db; +} + +function roundHours(value: number) { + return Number(value.toFixed(6)); +} + +async function computeVmUsage( + db: ReturnType, + vmId: string, + periodStart: number, + periodEnd: number +): Promise { + const [vm] = await db + .select({ + id: vms.id, + name: vms.name, + projectId: vms.ownerProjectId, + projectName: organization.name, + vmTypeFeatureId: vmTypes.autumnFeatureId, + ratePerHour: vmTypes.rate + }) + .from(vms) + .leftJoin(vmTypes, eq(vmTypes.id, vms.vmTypeId)) + .leftJoin(organization, eq(organization.id, vms.ownerProjectId)) + .where(eq(vms.id, vmId)) + .limit(1); + if (!vm) error(404, `VM "${vmId}" not found`); + + const events = await db + .select({ + quantity: billingUsageEvents.quantity, + featureId: billingUsageEvents.featureId + }) + .from(billingUsageEvents) + .where( + and( + eq(billingUsageEvents.resourceType, 'vm'), + eq(billingUsageEvents.resourceId, vmId), + gt(billingUsageEvents.periodEnd, periodStart), + lte(billingUsageEvents.periodEnd, periodEnd) + ) + ) + .orderBy(desc(billingUsageEvents.periodEnd)); + + let billedHours = 0; + let reversedHours = 0; + let eventCount = 0; + let eventFeatureId: string | null = null; + for (const event of events) { + const quantity = Number(event.quantity); + if (quantity >= 0) { + billedHours += quantity; + eventCount += 1; + eventFeatureId ??= event.featureId; + } else { + reversedHours += -quantity; + } + } + + billedHours = roundHours(billedHours); + reversedHours = roundHours(reversedHours); + const reversibleHours = roundHours(Math.max(0, billedHours - reversedHours)); + const featureId = eventFeatureId ?? vm.vmTypeFeatureId; + const rate = vm.ratePerHour == null ? null : Number(vm.ratePerHour); + + return { + vmId: vm.id, + vmName: vm.name, + projectId: vm.projectId, + projectName: vm.projectName, + featureId, + periodStart, + periodEnd, + billedHours, + reversedHours, + reversibleHours, + eventCount, + ratePerHour: vm.ratePerHour, + estimatedAmount: + rate == null || Number.isNaN(rate) ? null : Number((reversibleHours * rate).toFixed(2)) + }; +} + +function validateWindow(periodStart: number, periodEnd: number) { + if (periodStart >= periodEnd) error(400, 'The window start must be before its end'); + if (periodEnd > Date.now() + 60_000) error(400, 'The window cannot end in the future'); +} + +const usageParams = type({ vmId: 'string', periodStart: 'number', periodEnd: 'number' }); + +export const getVmBillingUsage = query(usageParams, async (params) => { + const db = await requireCurrentAdmin(); + validateWindow(params.periodStart, params.periodEnd); + + return computeVmUsage(db, params.vmId, params.periodStart, params.periodEnd); +}); + +const reverseParams = type({ + vmId: 'string', + periodStart: 'number', + periodEnd: 'number', + 'note?': 'string' +}); + +export const reverseVmBillingUsage = command(reverseParams, async (params) => { + const db = await requireCurrentAdmin(); + validateWindow(params.periodStart, params.periodEnd); + + const usage = await computeVmUsage(db, params.vmId, params.periodStart, params.periodEnd); + if (usage.reversibleHours <= 0) error(400, 'No reversible usage in this window'); + if (!usage.featureId) error(400, 'This VM has no billing feature to reverse against'); + if (!usage.projectId) error(400, 'This VM has no project to credit'); + + const now = Date.now(); + const idempotencyKey = [ + 'reversal', + 'vm', + params.vmId, + usage.featureId, + params.periodStart, + params.periodEnd, + usage.reversibleHours + ].join(':'); + + const [event] = await db + .insert(billingUsageEvents) + .values({ + projectId: usage.projectId, + resourceType: 'vm', + resourceId: params.vmId, + featureId: usage.featureId, + quantity: (-usage.reversibleHours).toString(), + periodStart: params.periodStart, + periodEnd: params.periodEnd, + idempotencyKey, + note: params.note?.trim() || null, + createdAt: now + }) + .onConflictDoNothing({ target: billingUsageEvents.idempotencyKey }) + .returning(); + if (!event) error(409, 'An identical reversal was already recorded'); + + const syncStatus = await syncUsageEvent(event.id); + + return { + reversedHours: usage.reversibleHours, + estimatedAmount: usage.estimatedAmount, + syncStatus + }; +}); diff --git a/apps/dashboard/src/lib/remote/admin-projects.remote.ts b/apps/dashboard/src/lib/remote/admin-projects.remote.ts new file mode 100644 index 0000000..496b109 --- /dev/null +++ b/apps/dashboard/src/lib/remote/admin-projects.remote.ts @@ -0,0 +1,321 @@ +import { command, getRequestEvent, query } from '$app/server'; +import { error } from '@sveltejs/kit'; +import { type } from 'arktype'; +import { and, count, desc, eq } from 'drizzle-orm'; +import AdminProjectDeletionCodeEmail from '$lib/emails/admin-project-deletion-code.svelte'; +import { + ADMIN_VERIFICATION_CODE_TTL_MS, + beginAdminVerification, + consumeAdminVerification +} from '$lib/server/admin-verification'; +import { initAuth } from '$lib/server/auth'; +import { requireAdmin } from '$lib/server/auth-context'; +import { + ensureLocalProjectBillingCustomer, + ensureProjectCustomer, + isProjectBillingExempt, + requireProjectBillingActive +} from '$lib/server/billing/autumn'; +import { initDrizzle } from '$lib/server/db'; +import { + member, + organization, + projectBillingCustomers, + sshKeys, + user, + vms, + volumes +} from '$lib/server/db/schema'; +import { sendRenderedEmail } from '$lib/server/email'; +import { deleteOrganizationResources } from '$lib/server/project-deletion'; +import { provisionVm } from '$lib/server/vm-provisioning'; + +export type AdminProjectBillingStatus = 'configured' | 'past_due' | 'suspended' | 'none'; + +export type AdminProject = { + id: string; + name: string; + slug: string; + createdAt: number; + ownerId: string | null; + ownerName: string | null; + ownerEmail: string | null; + ownerBillingExempt: boolean; + memberCount: number; + vmCount: number; + volumeCount: number; + billingStatus: AdminProjectBillingStatus; + billingExempt: boolean; + disabled: boolean; +}; + +async function requireCurrentAdmin() { + const event = getRequestEvent(); + if (!event?.locals.user) error(401, 'Authentication required'); + + const db = initDrizzle(); + await requireAdmin(db, event.locals.user.id); + + return db; +} + +function makeCountMap(rows: { key: string | null; count: number }[]) { + const map = new Map(); + for (const row of rows) { + if (!row.key) continue; + map.set(row.key, row.count); + } + return map; +} + +export const listAdminProjects = query(async (): Promise => { + const db = await requireCurrentAdmin(); + + const [orgs, owners, memberCounts, vmCounts, volumeCounts, billingCustomers] = await Promise.all([ + db + .select({ + id: organization.id, + name: organization.name, + slug: organization.slug, + createdAt: organization.createdAt, + billingExempt: organization.billingExempt, + disabled: organization.disabled + }) + .from(organization) + .orderBy(desc(organization.createdAt)), + db + .select({ + organizationId: member.organizationId, + userId: user.id, + name: user.name, + email: user.email, + billingExempt: user.billingExempt + }) + .from(member) + .innerJoin(user, eq(user.id, member.userId)) + .where(eq(member.role, 'owner')), + db + .select({ key: member.organizationId, count: count() }) + .from(member) + .groupBy(member.organizationId), + db + .select({ key: vms.ownerProjectId, count: count() }) + .from(vms) + .where(eq(vms.active, true)) + .groupBy(vms.ownerProjectId), + db + .select({ key: volumes.ownerProjectId, count: count() }) + .from(volumes) + .groupBy(volumes.ownerProjectId), + db + .select({ + projectId: projectBillingCustomers.projectId, + pastDueSince: projectBillingCustomers.pastDueSince, + suspendedAt: projectBillingCustomers.suspendedAt + }) + .from(projectBillingCustomers) + ]); + + const ownerByProject = new Map(owners.map((owner) => [owner.organizationId, owner])); + const memberMap = makeCountMap(memberCounts); + const vmMap = makeCountMap(vmCounts); + const volumeMap = makeCountMap(volumeCounts); + const billingByProject = new Map(billingCustomers.map((row) => [row.projectId, row])); + + return orgs.map((org) => { + const owner = ownerByProject.get(org.id); + const billing = billingByProject.get(org.id); + const billingStatus: AdminProjectBillingStatus = billing + ? billing.suspendedAt != null + ? 'suspended' + : billing.pastDueSince != null + ? 'past_due' + : 'configured' + : 'none'; + + return { + id: org.id, + name: org.name, + slug: org.slug, + createdAt: org.createdAt.getTime(), + ownerId: owner?.userId ?? null, + ownerName: owner?.name ?? null, + ownerEmail: owner?.email ?? null, + ownerBillingExempt: owner?.billingExempt ?? false, + memberCount: memberMap.get(org.id) ?? 0, + vmCount: vmMap.get(org.id) ?? 0, + volumeCount: volumeMap.get(org.id) ?? 0, + billingStatus, + billingExempt: org.billingExempt, + disabled: org.disabled + }; + }); +}); + +const setBillingExemptParams = type({ projectId: 'string', billingExempt: 'boolean' }); +export const setProjectBillingExempt = command(setBillingExemptParams, async (params) => { + const db = await requireCurrentAdmin(); + + const target = await db.query.organization.findFirst({ + where: eq(organization.id, params.projectId) + }); + if (!target) error(404, 'Project not found'); + + await db + .update(organization) + .set({ billingExempt: params.billingExempt }) + .where(eq(organization.id, params.projectId)); + + return { projectId: params.projectId, billingExempt: params.billingExempt }; +}); + +function toSlug(name: string) { + const slug = name + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') + .slice(0, 42); + + return `${slug || 'project'}-${Date.now().toString(36)}`; +} + +const createProjectParams = type({ name: 'string', ownerUserId: 'string' }); +export const createAdminProject = command(createProjectParams, async (params) => { + const db = await requireCurrentAdmin(); + + const owner = await db.query.user.findFirst({ where: eq(user.id, params.ownerUserId) }); + if (!owner) error(404, 'User not found'); + + const name = params.name.trim() || 'Untitled Project'; + const auth = initAuth(); + const org = await auth.api.createOrganization({ + body: { + name, + slug: toSlug(name), + userId: params.ownerUserId + } + }); + if (!org) error(502, 'Failed to create project'); + + await ensureLocalProjectBillingCustomer(org.id); + ensureProjectCustomer(org.id).catch((err) => { + console.warn(`Failed to sync Autumn customer for project ${org.id}`, err); + }); + + return { id: org.id }; +}); + +const createVmParams = type({ + projectId: 'string', + vmTypeId: 'string', + name: 'string', + networkingMode: "'both' | 'ipv6'?", + imageId: 'string?', + sshKeyIds: 'string[]?', + password: 'string?' +}); +export const createAdminVm = command(createVmParams, async (params) => { + const db = await requireCurrentAdmin(); + const adminUser = getRequestEvent().locals.user; + if (!adminUser) error(401, 'Authentication required'); + + const project = await db.query.organization.findFirst({ + where: eq(organization.id, params.projectId) + }); + if (!project) error(404, 'Project not found'); + if (project.disabled) error(400, 'This project is disabled'); + + const billingExempt = await isProjectBillingExempt(params.projectId); + if (!billingExempt) await requireProjectBillingActive(params.projectId); + + const [owner] = await db + .select({ userId: member.userId }) + .from(member) + .where(and(eq(member.organizationId, params.projectId), eq(member.role, 'owner'))) + .limit(1); + + const keys = + params.sshKeyIds?.length && owner + ? await db.query.sshKeys.findMany({ where: eq(sshKeys.userId, owner.userId) }) + : []; + const publicKeys = params.sshKeyIds?.length + ? keys.filter((key) => params.sshKeyIds!.includes(key.id)).map((key) => key.publicKey) + : []; + + return provisionVm(db, { + projectId: params.projectId, + vmTypeId: params.vmTypeId, + name: params.name, + userId: owner?.userId ?? adminUser.id, + billingExempt, + networkingMode: params.networkingMode, + imageId: params.imageId, + sshPublicKeys: publicKeys, + password: params.password + }); +}); + +const setDisabledParams = type({ projectId: 'string', disabled: 'boolean' }); +export const setProjectDisabled = command(setDisabledParams, async (params) => { + const db = await requireCurrentAdmin(); + + const target = await db.query.organization.findFirst({ + where: eq(organization.id, params.projectId) + }); + if (!target) error(404, 'Project not found'); + + await db + .update(organization) + .set({ disabled: params.disabled }) + .where(eq(organization.id, params.projectId)); + + return { projectId: params.projectId, disabled: params.disabled }; +}); + +const beginDeleteProjectParams = type({ projectId: 'string' }); +export const beginDeleteProject = command(beginDeleteProjectParams, async (params) => { + const db = await requireCurrentAdmin(); + const adminUser = getRequestEvent().locals.user; + if (!adminUser) error(401, 'Authentication required'); + + const target = await db.query.organization.findFirst({ + where: eq(organization.id, params.projectId) + }); + if (!target) error(404, 'Project not found'); + + const { method, code } = await beginAdminVerification(db, adminUser.id, params.projectId); + + if (code) { + await sendRenderedEmail({ + component: AdminProjectDeletionCodeEmail, + props: { + userName: adminUser.name, + projectName: target.name, + code, + expiresInMinutes: ADMIN_VERIFICATION_CODE_TTL_MS / 60_000 + }, + subject: 'Confirm Stack project deletion', + to: adminUser.email + }); + } + + return { method, email: adminUser.email, projectName: target.name }; +}); + +const deleteProjectParams = type({ projectId: 'string', method: 'string', code: 'string?' }); +export const deleteProjectWithVerification = command(deleteProjectParams, async (params) => { + const db = await requireCurrentAdmin(); + const adminUser = getRequestEvent().locals.user; + if (!adminUser) error(401, 'Authentication required'); + + const target = await db.query.organization.findFirst({ + where: eq(organization.id, params.projectId) + }); + if (!target) error(404, 'Project not found'); + + await consumeAdminVerification(db, adminUser.id, params.projectId, params.method, params.code); + await deleteOrganizationResources(db, params.projectId); + + return { projectId: params.projectId, name: target.name }; +}); diff --git a/apps/dashboard/src/lib/remote/admin-users.remote.ts b/apps/dashboard/src/lib/remote/admin-users.remote.ts index 4d0cc2b..3ab2872 100644 --- a/apps/dashboard/src/lib/remote/admin-users.remote.ts +++ b/apps/dashboard/src/lib/remote/admin-users.remote.ts @@ -1,45 +1,30 @@ import { command, getRequestEvent, query } from '$app/server'; import { error } from '@sveltejs/kit'; import { type } from 'arktype'; -import { and, asc, count, desc, eq, gt, inArray } from 'drizzle-orm'; +import { asc, count, desc, eq } from 'drizzle-orm'; import AdminUserDeletionCodeEmail from '$lib/emails/admin-user-deletion-code.svelte'; +import { + ADMIN_VERIFICATION_CODE_TTL_MS, + beginAdminVerification, + consumeAdminVerification +} from '$lib/server/admin-verification'; import { hasAdminRole, requireAdmin } from '$lib/server/auth-context'; -import { initAuth } from '$lib/server/auth'; import { initDrizzle } from '$lib/server/db'; import { account, apiTokens, - invitation, - ipAssignments, member, organization, passkey, - paymentPeriods, session, sshKeys, - twoFactor, user, - verification, volumes, vms } from '$lib/server/db/schema'; -import { getBackend } from '$lib/server/backends'; -import { - cancelProjectBilling, - deleteLocalProjectBillingCustomer, - deleteProjectServerEntity, - updateProjectCustomer -} from '$lib/server/billing/autumn'; -import { meterResourceThrough, syncProjectUsage } from '$lib/server/billing/metering'; +import { updateProjectCustomer } from '$lib/server/billing/autumn'; import { sendRenderedEmail } from '$lib/server/email'; -import { ulid } from '$lib/server/id'; -import { releaseVmNetworking } from '$lib/server/ipam'; - -const CODE_LENGTH = 6; -const USER_DELETE_CODE_TTL_MS = 10 * 60 * 1000; -const USER_DELETE_INTENT_TTL_MS = 60 * 1000; - -type UserDeletionVerificationMethod = 'passkey' | 'totp' | 'email'; +import { deleteOrganizationResources } from '$lib/server/project-deletion'; export type UserSession = { id: string; @@ -104,55 +89,6 @@ async function requireCurrentAdmin() { return { db, userId: event.locals.user.id }; } -function adminUserDeletionIntentIdentifier(adminUserId: string) { - return `admin-user-delete-intent:${adminUserId}`; -} - -function adminUserDeletionPasskeyIdentifier(adminUserId: string, targetUserId: string) { - return `admin-user-delete-passkey:${adminUserId}:${targetUserId}`; -} - -function adminUserDeletionEmailIdentifier(adminUserId: string, targetUserId: string) { - return `admin-user-delete-email:${adminUserId}:${targetUserId}`; -} - -function generateCode() { - const max = 10 ** CODE_LENGTH; - const value = crypto.getRandomValues(new Uint32Array(1))[0] % max; - return value.toString().padStart(CODE_LENGTH, '0'); -} - -function normalizeCode(code: string) { - return code.replace(/\D/g, ''); -} - -async function hashCode(adminUserId: string, targetUserId: string, code: string) { - const data = new TextEncoder().encode(`${adminUserId}:${targetUserId}:${code}`); - const hash = await crypto.subtle.digest('SHA-256', data); - return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, '0')).join(''); -} - -async function getDeletionVerificationMethod( - db: ReturnType, - adminUserId: string -): Promise { - const [registeredPasskey] = await db - .select({ id: passkey.id }) - .from(passkey) - .where(eq(passkey.userId, adminUserId)) - .limit(1); - - if (registeredPasskey) return 'passkey'; - - const [registeredTotp] = await db - .select({ id: twoFactor.id }) - .from(twoFactor) - .where(eq(twoFactor.userId, adminUserId)) - .limit(1); - - return registeredTotp ? 'totp' : 'email'; -} - async function assertCanDeleteUser( db: ReturnType, adminUserId: string, @@ -174,54 +110,6 @@ async function assertCanDeleteUser( return target; } -async function verifyDeletionEmailCode( - db: ReturnType, - adminUserId: string, - targetUserId: string, - code: string -) { - const normalizedCode = normalizeCode(code); - if (normalizedCode.length !== CODE_LENGTH) - error(400, 'Enter the verification code from your email.'); - - const identifier = adminUserDeletionEmailIdentifier(adminUserId, targetUserId); - const value = await hashCode(adminUserId, targetUserId, normalizedCode); - const [record] = await db - .select({ id: verification.id }) - .from(verification) - .where( - and( - eq(verification.identifier, identifier), - eq(verification.value, value), - gt(verification.expiresAt, new Date()) - ) - ) - .limit(1); - - if (!record) error(400, 'Invalid or expired verification code.'); - await db.delete(verification).where(eq(verification.id, record.id)); -} - -async function verifyDeletionPasskey( - db: ReturnType, - adminUserId: string, - targetUserId: string -) { - const [record] = await db - .select({ id: verification.id }) - .from(verification) - .where( - and( - eq(verification.identifier, adminUserDeletionPasskeyIdentifier(adminUserId, targetUserId)), - gt(verification.expiresAt, new Date()) - ) - ) - .limit(1); - - if (!record) error(400, 'Verify with your passkey before deleting this user.'); - await db.delete(verification).where(eq(verification.id, record.id)); -} - async function deleteUserData(db: ReturnType, targetUserId: string) { await settleUserOrganizations(db, targetUserId); await db.delete(apiTokens).where(eq(apiTokens.userId, targetUserId)); @@ -258,67 +146,6 @@ async function settleUserOrganizations(db: ReturnType, targe } } -async function deleteOrganizationResources( - db: ReturnType, - organizationId: string -) { - const projectVms = await db.query.vms.findMany({ - where: eq(vms.ownerProjectId, organizationId), - columns: { - id: true, - name: true, - proxmoxId: true, - active: true, - backend: true - } - }); - const vmIds = projectVms.map((vm) => vm.id); - - for (const vm of projectVms.filter((item) => item.active)) { - try { - await getBackend(vm.backend).deleteVm(vm.id, vm.proxmoxId ?? undefined); - } catch (err) { - console.warn(`Failed to deprovision VM ${vm.id} during user delete`, err); - error(502, `Failed to deprovision VM "${vm.name}" in Proxmox`); - } - } - - for (const vm of projectVms.filter((item) => item.active)) { - const metered = await meterResourceThrough('vm', vm.id).catch((err) => { - console.warn(`Failed to meter VM ${vm.id} during user delete`, err); - error(502, `Failed to meter VM "${vm.name}" during user delete`); - }); - if (!metered?.event || metered.syncStatus === 'synced') { - await deleteProjectServerEntity(organizationId, vm.id).catch((err) => { - console.warn(`Failed to delete Autumn entity for VM ${vm.id}`, err); - }); - } - } - await syncProjectUsage(organizationId); - - await db.delete(volumes).where(eq(volumes.ownerProjectId, organizationId)); - for (const vm of projectVms) { - await releaseVmNetworking(db, vm.id).catch((err) => { - console.warn(`Failed to release networking for VM ${vm.id} during user delete`, err); - }); - } - if (vmIds.length > 0) { - await db.delete(ipAssignments).where(inArray(ipAssignments.associatedVmId, vmIds)); - await db.delete(paymentPeriods).where(inArray(paymentPeriods.vmId, vmIds)); - } - await db.delete(vms).where(eq(vms.ownerProjectId, organizationId)); - await db.delete(invitation).where(eq(invitation.organizationId, organizationId)); - await db.delete(member).where(eq(member.organizationId, organizationId)); - const billingCancelled = await cancelProjectBilling(organizationId).catch((err) => { - console.warn(`Failed to cancel Autumn billing for project ${organizationId}`, err); - return false; - }); - if (billingCancelled) { - await deleteLocalProjectBillingCustomer(organizationId); - } - await db.delete(organization).where(eq(organization.id, organizationId)); -} - function makeCountMap(rows: { userId: string | null; count: number }[]) { const map = new Map(); for (const row of rows) { @@ -469,39 +296,16 @@ export const beginDeleteUser = command(beginDeleteUserParams, async (params) => if (!adminUser) error(401, 'Authentication required'); const target = await assertCanDeleteUser(db, adminUserId, params.userId); - const method = await getDeletionVerificationMethod(db, adminUserId); - - await db - .delete(verification) - .where(eq(verification.identifier, adminUserDeletionIntentIdentifier(adminUserId))); - - if (method === 'passkey') { - await db.insert(verification).values({ - id: ulid(), - identifier: adminUserDeletionIntentIdentifier(adminUserId), - value: params.userId, - expiresAt: new Date(Date.now() + USER_DELETE_INTENT_TTL_MS) - }); - } else if (method === 'email') { - const code = generateCode(); - const identifier = adminUserDeletionEmailIdentifier(adminUserId, params.userId); - const value = await hashCode(adminUserId, params.userId, code); - - await db.delete(verification).where(eq(verification.identifier, identifier)); - await db.insert(verification).values({ - id: ulid(), - identifier, - value, - expiresAt: new Date(Date.now() + USER_DELETE_CODE_TTL_MS) - }); + const { method, code } = await beginAdminVerification(db, adminUserId, params.userId); + if (code) { await sendRenderedEmail({ component: AdminUserDeletionCodeEmail, props: { userName: adminUser.name, targetEmail: target.email, code, - expiresInMinutes: USER_DELETE_CODE_TTL_MS / 60_000 + expiresInMinutes: ADMIN_VERIFICATION_CODE_TTL_MS / 60_000 }, subject: 'Confirm Stack user deletion', to: adminUser.email @@ -515,27 +319,8 @@ const deleteUserParams = type({ userId: 'string', method: 'string', code: 'strin export const deleteUserWithVerification = command(deleteUserParams, async (params) => { const { db, userId: adminUserId } = await requireCurrentAdmin(); const target = await assertCanDeleteUser(db, adminUserId, params.userId); - const method = await getDeletionVerificationMethod(db, adminUserId); - - if (params.method !== method) - error(400, 'Use the required verification method for this account.'); - - if (method === 'passkey') { - await verifyDeletionPasskey(db, adminUserId, params.userId); - } else if (method === 'totp') { - const code = normalizeCode(params.code ?? ''); - if (code.length !== CODE_LENGTH) - error(400, 'Enter the verification code from your authenticator app.'); - - const auth = initAuth(); - await auth.api.verifyTOTP({ - headers: getRequestEvent().request.headers, - body: { code, trustDevice: false } - }); - } else { - await verifyDeletionEmailCode(db, adminUserId, params.userId, params.code ?? ''); - } + await consumeAdminVerification(db, adminUserId, params.userId, params.method, params.code); await deleteUserData(db, params.userId); return { userId: params.userId, email: target.email }; diff --git a/apps/dashboard/src/lib/remote/admin-vms.remote.ts b/apps/dashboard/src/lib/remote/admin-vms.remote.ts index f46d10c..c623741 100644 --- a/apps/dashboard/src/lib/remote/admin-vms.remote.ts +++ b/apps/dashboard/src/lib/remote/admin-vms.remote.ts @@ -12,16 +12,23 @@ export type AdminVm = { id: string; name: string; proxmoxId: number | null; + proxmoxNode: string | null; active: boolean; status: 'provisioning' | 'ready' | 'error' | 'deleting'; statusError: string | null; liveStatus: string | null; uptime: number; + cpuUsage: number | null; + memoryUsageBytes: number | null; + memoryTotalBytes: number | null; createdAt: number; + deletedAt: number | null; + lastKnownAt: number | null; lastKnownIpv4: string | null; lastKnownIpv6: string | null; projectId: string | null; projectName: string | null; + projectBillingExempt: boolean; ownerName: string | null; ownerEmail: string | null; ownerBillingExempt: boolean; @@ -29,6 +36,7 @@ export type AdminVm = { vmTypeCores: number | null; vmTypeRamCapacity: number | null; vmTypeStorageAmount: number | null; + vmTypeRate: string | null; }; async function requireCurrentAdmin() { @@ -50,21 +58,26 @@ export const listAllAdminVms = query(async (): Promise => { id: vms.id, name: vms.name, proxmoxId: vms.proxmoxId, + proxmoxNode: vms.proxmoxNode, active: vms.active, backend: vms.backend, status: vms.status, statusError: vms.statusError, lastKnownStatus: vms.lastKnownStatus, lastKnownUptime: vms.lastKnownUptime, + lastKnownAt: vms.lastKnownAt, createdAt: vms.createdAt, + deletedAt: vms.deletedAt, lastKnownIpv4: vms.lastKnownIpv4, lastKnownIpv6: vms.lastKnownIpv6, projectId: vms.ownerProjectId, projectName: organization.name, + projectBillingExempt: organization.billingExempt, vmTypeName: vmTypes.name, vmTypeCores: vmTypes.cores, vmTypeRamCapacity: vmTypes.ramCapacity, - vmTypeStorageAmount: vmTypes.storageAmount + vmTypeStorageAmount: vmTypes.storageAmount, + vmTypeRate: vmTypes.rate }) .from(vms) .leftJoin(vmTypes, eq(vmTypes.id, vms.vmTypeId)) @@ -116,6 +129,7 @@ export const listAllAdminVms = query(async (): Promise => { } } + const now = Date.now(); return rows.map((row) => { const live = row.proxmoxId != null ? liveByProxmoxId.get(row.proxmoxId) : null; const owner = row.projectId ? ownerByProject.get(row.projectId) : null; @@ -124,23 +138,31 @@ export const listAllAdminVms = query(async (): Promise => { id: row.id, name: row.name, proxmoxId: row.proxmoxId, + proxmoxNode: live?.proxmoxNode ?? row.proxmoxNode, active: row.active, status: row.status, statusError: row.statusError, liveStatus: row.active ? (live?.status ?? row.lastKnownStatus) : null, uptime: live?.uptime ?? row.lastKnownUptime ?? 0, + cpuUsage: row.active ? (live?.metrics?.cpu ?? null) : null, + memoryUsageBytes: row.active ? (live?.metrics?.memory ?? null) : null, + memoryTotalBytes: row.active ? (live?.memory ?? null) : null, createdAt: row.createdAt, + deletedAt: row.deletedAt, + lastKnownAt: live ? now : row.lastKnownAt, lastKnownIpv4: row.lastKnownIpv4, lastKnownIpv6: row.lastKnownIpv6, projectId: row.projectId, projectName: row.projectName, + projectBillingExempt: row.projectBillingExempt ?? false, ownerName: owner?.name ?? null, ownerEmail: owner?.email ?? null, ownerBillingExempt: owner?.billingExempt ?? false, vmTypeName: row.vmTypeName, vmTypeCores: row.vmTypeCores, vmTypeRamCapacity: row.vmTypeRamCapacity, - vmTypeStorageAmount: row.vmTypeStorageAmount + vmTypeStorageAmount: row.vmTypeStorageAmount, + vmTypeRate: row.vmTypeRate }; }); }); diff --git a/apps/dashboard/src/lib/remote/email-campaign.remote.ts b/apps/dashboard/src/lib/remote/email-campaign.remote.ts index 4cba7f3..6f05956 100644 --- a/apps/dashboard/src/lib/remote/email-campaign.remote.ts +++ b/apps/dashboard/src/lib/remote/email-campaign.remote.ts @@ -9,6 +9,7 @@ import OrganizationInvitationEmail from '$lib/emails/organization-invitation.sve import PasswordChangeCodeEmail from '$lib/emails/password-change-code.svelte'; import ResetPasswordEmail from '$lib/emails/reset-password.svelte'; import SecurityAlertEmail from '$lib/emails/security-alert.svelte'; +import ServiceDisruptionEmail from '$lib/emails/service-disruption.svelte'; import VerifyEmailEmail from '$lib/emails/verify-email.svelte'; import { applyPlaceholders, @@ -32,6 +33,7 @@ const templateComponents: Record = { 'password-change-code': PasswordChangeCodeEmail, 'reset-password': ResetPasswordEmail, 'security-alert': SecurityAlertEmail, + 'service-disruption': ServiceDisruptionEmail, 'verify-email': VerifyEmailEmail }; diff --git a/apps/dashboard/src/lib/remote/vms.remote.ts b/apps/dashboard/src/lib/remote/vms.remote.ts index a416d79..9305bb0 100644 --- a/apps/dashboard/src/lib/remote/vms.remote.ts +++ b/apps/dashboard/src/lib/remote/vms.remote.ts @@ -1,10 +1,10 @@ import { query, command, getRequestEvent } from '$app/server'; import { error } from '@sveltejs/kit'; import { type } from 'arktype'; -import { and, eq, sql } from 'drizzle-orm'; -import { initDrizzle, closeRequestDb, type Database } from '$lib/server/db'; +import { eq, sql } from 'drizzle-orm'; +import { initDrizzle, type Database } from '$lib/server/db'; import { runInBackground } from '$lib/server/background'; -import { vms, vmTypes, sshKeys, baseImages } from '$lib/server/db/schema'; +import { vms, vmTypes, sshKeys } from '$lib/server/db/schema'; import { getBackend, type VmBackend, @@ -12,16 +12,9 @@ import { type VmMetricsTimeframe } from '$lib/server/backends'; import { requireProjectAccess } from '$lib/server/auth-context'; -import { - deleteProjectServerEntity, - ensureProjectServerEntity, - isBillingConfigured, - isProjectBillingExempt, - requireProjectBillingActive -} from '$lib/server/billing/autumn'; -import { createBillingMeter } from '$lib/server/billing/metering'; -import { allocateVmNetworking, generateMacAddress, releaseVmNetworking } from '$lib/server/ipam'; +import { isProjectBillingExempt, requireProjectBillingActive } from '$lib/server/billing/autumn'; import { queueVmDeletion } from '$lib/server/vm-deletion'; +import { provisionVm } from '$lib/server/vm-provisioning'; import { instrument, timingLog } from '$lib/server/observability'; type VmRow = { @@ -491,184 +484,26 @@ export const createVm = command(createParams, async (params) => { const billingExempt = await isProjectBillingExempt(params.projectId); if (!billingExempt) await requireProjectBillingActive(params.projectId); - const [vmType, baseImage, keys] = await Promise.all([ - db.query.vmTypes.findFirst({ - where: eq(vmTypes.id, params.vmTypeId) - }), - params.imageId - ? db.query.baseImages.findFirst({ - where: eq(baseImages.id, params.imageId) - }) - : null, - params.sshKeyIds?.length - ? db.query.sshKeys.findMany({ - where: eq(sshKeys.userId, event.locals.user.id) - }) - : [] - ]); - if (!vmType) error(400, `VM type "${params.vmTypeId}" not found`); - if (isBillingConfigured() && !vmType.autumnFeatureId) - error(400, `VM type "${vmType.name}" is missing an Autumn feature ID`); - const featureId = vmType.autumnFeatureId; - - if (params.imageId && !baseImage) error(400, `Image "${params.imageId}" not found`); - + const keys = params.sshKeyIds?.length + ? await db.query.sshKeys.findMany({ + where: eq(sshKeys.userId, event.locals.user.id) + }) + : []; const publicKeys = params.sshKeyIds?.length ? keys.filter((key) => params.sshKeyIds!.includes(key.id)).map((key) => key.publicKey) : []; - const now = Date.now(); - const [inserted] = await db - .insert(vms) - .values({ - name: params.name, - proxmoxId: null, - active: true, - ownerProjectId: params.projectId, - vmTypeId: params.vmTypeId, - creationDate: new Date().toISOString().split('T')[0], - createdAt: now, - backend: 'proxmox', - status: 'provisioning' - }) - .returning(); - - const vmId = inserted.id; - const macAddress = generateMacAddress(); - let networkingAllocations: Awaited> = []; - let result; - try { - if (!billingExempt) { - await ensureProjectServerEntity({ - projectId: params.projectId, - serverId: vmId, - name: params.name - }); - } - networkingAllocations = await allocateVmNetworking(db, { - vmId, - macAddress, - mode: params.networkingMode ?? 'both' - }); - const ipv4NetworkAllocation = networkingAllocations.find( - (allocation) => allocation.family === 'ipv4' && allocation.address - ); - const ipv6TransitNetworkAllocation = networkingAllocations.find( - (allocation) => allocation.family === 'ipv6' && allocation.address - ); - const ipv6PrefixNetworkAllocation = networkingAllocations.find( - (allocation) => allocation.family === 'ipv6' && allocation.prefix - ); - const firewallIpSet = [ - ...(ipv4NetworkAllocation?.address ? [`${ipv4NetworkAllocation.address}/32`] : []), - ...(ipv6TransitNetworkAllocation?.address - ? [`${ipv6TransitNetworkAllocation.address}/128`] - : []), - ...(ipv6PrefixNetworkAllocation?.prefix ? [ipv6PrefixNetworkAllocation.prefix] : []) - ]; - const backend = getBackend('proxmox'); - result = await backend.createVm({ - id: vmId, - name: params.name, - proxmoxId: inserted.proxmoxId ?? undefined, - macAddress, - cores: vmType.cores, - memoryMb: vmType.ramCapacity, - diskGb: vmType.storageAmount, - imageId: params.imageId, - imageSource: baseImage?.filePath, - secureBoot: baseImage?.secureBoot ?? true, - networkConfig: { - firewallIpSet, - ...(ipv4NetworkAllocation?.address - ? { - ipv4: { - address: ipv4NetworkAllocation.address, - prefixLength: ipv4NetworkAllocation.prefixLength, - gateway: ipv4NetworkAllocation.sourcePrefix.gatewayAddress ?? '' - } - } - : {}), - ...(ipv6TransitNetworkAllocation?.address - ? { - ipv6: { - address: ipv6TransitNetworkAllocation.address, - prefixLength: ipv6TransitNetworkAllocation.prefixLength - } - } - : {}), - ...(ipv6PrefixNetworkAllocation?.prefix - ? { ipv6Prefix: ipv6PrefixNetworkAllocation.prefix } - : {}) - }, - sshKeys: publicKeys, - password: params.password, - onProvisionSettled: async ({ ok, error: err }) => { - const settledEvent = getRequestEvent(); - const settledDb = initDrizzle(); - try { - await settledDb - .update(vms) - .set( - ok ? { status: 'ready' } : { status: 'error', statusError: err ?? 'Unknown error' } - ) - .where(and(eq(vms.id, vmId), eq(vms.active, true))); - console.log(`VM ${vmId} provision ${ok ? 'succeeded' : 'failed'}`); - } catch (updateErr) { - console.error(`VM ${vmId} status update failed:`, updateErr); - } finally { - closeRequestDb(settledEvent); - } - }, - registerBackground: runInBackground, - userId: event.locals.user.id, - projectId: params.projectId - }); - - if (!result.macAddress) error(502, 'Proxmox did not return a MAC address'); - } catch (err) { - if (result?.proxmoxId != null) { - await getBackend('proxmox') - .deleteVm(vmId, result.proxmoxId) - .catch((deleteErr) => { - console.warn(`Failed to clean up Proxmox VM ${vmId} after provisioning error`, deleteErr); - }); - } - await releaseVmNetworking(db, vmId).catch(() => {}); - await deleteProjectServerEntity(params.projectId, vmId).catch(() => {}); - await db - .delete(vms) - .where(eq(vms.id, inserted.id)) - .catch(() => {}); - throw err; - } - - const ipv4Allocation = networkingAllocations.find((allocation) => allocation.family === 'ipv4'); - const ipv6Allocation = networkingAllocations.find( - (allocation) => allocation.family === 'ipv6' && allocation.address - ); - - await db - .update(vms) - .set({ - proxmoxId: result.proxmoxId ?? null, - proxmoxNode: result.proxmoxNode ?? null, - lastKnownIpv4: ipv4Allocation?.address ?? null, - lastKnownIpv6: ipv6Allocation?.address ?? null - }) - .where(eq(vms.id, vmId)); - if (featureId) { - await createBillingMeter({ - projectId: params.projectId, - resourceType: 'vm', - resourceId: vmId, - featureId, - units: 1, - now - }); - } - - return { id: inserted.id, taskId: result.taskId }; + return provisionVm(db, { + projectId: params.projectId, + vmTypeId: params.vmTypeId, + name: params.name, + userId: event.locals.user.id, + billingExempt, + networkingMode: params.networkingMode, + imageId: params.imageId, + sshPublicKeys: publicKeys, + password: params.password + }); }); const deleteParams = type({ vmId: 'string' }); diff --git a/apps/dashboard/src/lib/server/admin-verification.ts b/apps/dashboard/src/lib/server/admin-verification.ts new file mode 100644 index 0000000..b9deab5 --- /dev/null +++ b/apps/dashboard/src/lib/server/admin-verification.ts @@ -0,0 +1,183 @@ +import { error } from '@sveltejs/kit'; +import { and, eq, gt } from 'drizzle-orm'; +import { getRequestEvent } from '$app/server'; +import { initAuth } from '$lib/server/auth'; +import type { initDrizzle } from '$lib/server/db'; +import { passkey, twoFactor, verification } from '$lib/server/db/schema'; +import { ulid } from '$lib/server/id'; + +export const ADMIN_VERIFICATION_CODE_LENGTH = 6; +export const ADMIN_VERIFICATION_CODE_TTL_MS = 10 * 60 * 1000; +const ADMIN_VERIFICATION_INTENT_TTL_MS = 60 * 1000; + +export type AdminVerificationMethod = 'passkey' | 'totp' | 'email'; + +type Db = ReturnType; + +function intentIdentifier(adminUserId: string) { + return `admin-user-delete-intent:${adminUserId}`; +} + +function passkeyIdentifier(adminUserId: string, targetId: string) { + return `admin-user-delete-passkey:${adminUserId}:${targetId}`; +} + +function emailIdentifier(adminUserId: string, targetId: string) { + return `admin-user-delete-email:${adminUserId}:${targetId}`; +} + +export function normalizeVerificationCode(code: string) { + return code.replace(/\D/g, ''); +} + +function generateVerificationCode() { + const max = 10 ** ADMIN_VERIFICATION_CODE_LENGTH; + const upperBound = 0x1_0000_0000; + const limit = Math.floor(upperBound / max) * max; + let value: number; + + do { + value = crypto.getRandomValues(new Uint32Array(1))[0]; + } while (value >= limit); + + return (value % max).toString().padStart(ADMIN_VERIFICATION_CODE_LENGTH, '0'); +} + +async function hashVerificationCode(adminUserId: string, targetId: string, code: string) { + const data = new TextEncoder().encode(`${adminUserId}:${targetId}:${code}`); + const hash = await crypto.subtle.digest('SHA-256', data); + return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, '0')).join(''); +} + +export async function getAdminVerificationMethod( + db: Db, + adminUserId: string +): Promise { + const [registeredPasskey] = await db + .select({ id: passkey.id }) + .from(passkey) + .where(eq(passkey.userId, adminUserId)) + .limit(1); + + if (registeredPasskey) return 'passkey'; + + const [registeredTotp] = await db + .select({ id: twoFactor.id }) + .from(twoFactor) + .where(eq(twoFactor.userId, adminUserId)) + .limit(1); + + return registeredTotp ? 'totp' : 'email'; +} + +export async function beginAdminVerification( + db: Db, + adminUserId: string, + targetId: string +): Promise<{ method: AdminVerificationMethod; code: string | null }> { + const method = await getAdminVerificationMethod(db, adminUserId); + + await db.delete(verification).where(eq(verification.identifier, intentIdentifier(adminUserId))); + + if (method === 'passkey') { + await db.insert(verification).values({ + id: ulid(), + identifier: intentIdentifier(adminUserId), + value: targetId, + expiresAt: new Date(Date.now() + ADMIN_VERIFICATION_INTENT_TTL_MS) + }); + return { method, code: null }; + } + + if (method === 'email') { + const code = generateVerificationCode(); + const identifier = emailIdentifier(adminUserId, targetId); + const value = await hashVerificationCode(adminUserId, targetId, code); + + await db.delete(verification).where(eq(verification.identifier, identifier)); + await db.insert(verification).values({ + id: ulid(), + identifier, + value, + expiresAt: new Date(Date.now() + ADMIN_VERIFICATION_CODE_TTL_MS) + }); + return { method, code }; + } + + return { method, code: null }; +} + +async function consumePasskeyVerification(db: Db, adminUserId: string, targetId: string) { + const [record] = await db + .select({ id: verification.id }) + .from(verification) + .where( + and( + eq(verification.identifier, passkeyIdentifier(adminUserId, targetId)), + gt(verification.expiresAt, new Date()) + ) + ) + .limit(1); + + if (!record) error(400, 'Verify with your passkey before performing this action.'); + await db.delete(verification).where(eq(verification.id, record.id)); +} + +async function consumeEmailVerification( + db: Db, + adminUserId: string, + targetId: string, + code: string +) { + const normalizedCode = normalizeVerificationCode(code); + if (normalizedCode.length !== ADMIN_VERIFICATION_CODE_LENGTH) + error(400, 'Enter the verification code from your email.'); + + const identifier = emailIdentifier(adminUserId, targetId); + const value = await hashVerificationCode(adminUserId, targetId, normalizedCode); + const [record] = await db + .select({ id: verification.id }) + .from(verification) + .where( + and( + eq(verification.identifier, identifier), + eq(verification.value, value), + gt(verification.expiresAt, new Date()) + ) + ) + .limit(1); + + if (!record) error(400, 'Invalid or expired verification code.'); + await db.delete(verification).where(eq(verification.id, record.id)); +} + +export async function consumeAdminVerification( + db: Db, + adminUserId: string, + targetId: string, + method: string, + code: string | undefined +) { + const required = await getAdminVerificationMethod(db, adminUserId); + if (method !== required) error(400, 'Use the required verification method for this account.'); + + if (required === 'passkey') { + await consumePasskeyVerification(db, adminUserId, targetId); + return; + } + + if (required === 'totp') { + const normalizedCode = normalizeVerificationCode(code ?? ''); + if (normalizedCode.length !== ADMIN_VERIFICATION_CODE_LENGTH) + error(400, 'Enter the verification code from your authenticator app.'); + + const auth = initAuth(); + await auth.api.verifyTOTP({ + headers: getRequestEvent().request.headers, + body: { code: normalizedCode, trustDevice: false } + }); + return; + } + + await consumeEmailVerification(db, adminUserId, targetId, code ?? ''); +} diff --git a/apps/dashboard/src/lib/server/auth-context.ts b/apps/dashboard/src/lib/server/auth-context.ts index ef39aa5..5c38098 100644 --- a/apps/dashboard/src/lib/server/auth-context.ts +++ b/apps/dashboard/src/lib/server/auth-context.ts @@ -19,14 +19,15 @@ function cachedLookup(key: string, compute: () => Promise): Promise { return lookup; } -type ProjectAccess = { projectId: string; role: string | null } | null; +type ProjectAccess = { projectId: string; role: string | null; disabled: boolean } | null; function loadProjectAccess(db: any, userId: string, projectId: string): Promise { return cachedLookup(`project-access:${userId}:${projectId}`, async () => { const [projectAccess] = await db .select({ projectId: organization.id, - role: member.role + role: member.role, + disabled: organization.disabled }) .from(organization) .leftJoin(member, and(eq(member.organizationId, organization.id), eq(member.userId, userId))) @@ -68,6 +69,10 @@ export async function requireProjectAccess( error(404, `Project "${projectId}" not found`); } + if (projectAccess.disabled) { + error(403, 'This project has been disabled'); + } + if (!projectAccess.role || !hasProjectRole(projectAccess.role, minLevel)) { error(403, 'Insufficient project permissions'); } diff --git a/apps/dashboard/src/lib/server/billing/autumn.ts b/apps/dashboard/src/lib/server/billing/autumn.ts index 409cf24..53cb57c 100644 --- a/apps/dashboard/src/lib/server/billing/autumn.ts +++ b/apps/dashboard/src/lib/server/billing/autumn.ts @@ -390,6 +390,14 @@ export async function getProjectBillingState( export async function isProjectBillingExempt(projectId: string) { const db = initDrizzle(); + + const [project] = await db + .select({ billingExempt: organization.billingExempt }) + .from(organization) + .where(eq(organization.id, projectId)) + .limit(1); + if (project?.billingExempt) return true; + const [exemptOwner] = await db .select({ userId: member.userId }) .from(member) diff --git a/apps/dashboard/src/lib/server/billing/metering.ts b/apps/dashboard/src/lib/server/billing/metering.ts index a4de1cb..4d242ef 100644 --- a/apps/dashboard/src/lib/server/billing/metering.ts +++ b/apps/dashboard/src/lib/server/billing/metering.ts @@ -343,7 +343,8 @@ async function trackUsageEvent(event: BillingUsageEvent) { resourceType: event.resourceType, resourceId: event.resourceId, periodStart: event.periodStart, - periodEnd: event.periodEnd + periodEnd: event.periodEnd, + ...(event.note ? { note: event.note } : {}) } }; diff --git a/apps/dashboard/src/lib/server/db/auth.schema.ts b/apps/dashboard/src/lib/server/db/auth.schema.ts index e589174..e7ccd7a 100644 --- a/apps/dashboard/src/lib/server/db/auth.schema.ts +++ b/apps/dashboard/src/lib/server/db/auth.schema.ts @@ -140,7 +140,9 @@ export const organization = pgTable( slug: text('slug').notNull().unique(), logo: text('logo'), createdAt: timestamp('created_at').notNull(), - metadata: text('metadata') + metadata: text('metadata'), + billingExempt: boolean('billing_exempt').default(false).notNull(), + disabled: boolean('disabled').default(false).notNull() }, (table) => [uniqueIndex('organization_slug_uidx').on(table.slug)] ); diff --git a/apps/dashboard/src/lib/server/db/schema.ts b/apps/dashboard/src/lib/server/db/schema.ts index da2e624..5aaa7f7 100644 --- a/apps/dashboard/src/lib/server/db/schema.ts +++ b/apps/dashboard/src/lib/server/db/schema.ts @@ -67,6 +67,7 @@ export const vms = pgTable( lastKnownUptime: integer('last_known_uptime').notNull().default(0), lastKnownAt: bigint('last_known_at', { mode: 'number' }), active: boolean('active').notNull().default(true), + deletedAt: bigint('deleted_at', { mode: 'number' }), ownerProjectId: ulidFk('owner_project_id').references(() => organization.id), vmTypeId: ulidFk('vm_type_id') .notNull() @@ -213,6 +214,7 @@ export const billingUsageEvents = pgTable( periodStart: bigint('period_start', { mode: 'number' }).notNull(), periodEnd: bigint('period_end', { mode: 'number' }).notNull(), idempotencyKey: text('idempotency_key').notNull(), + note: text('note'), syncStatus: billingSyncStatusEnum('sync_status').notNull().default('pending'), syncError: text('sync_error'), syncedAt: bigint('synced_at', { mode: 'number' }), diff --git a/apps/dashboard/src/lib/server/project-deletion.ts b/apps/dashboard/src/lib/server/project-deletion.ts new file mode 100644 index 0000000..38a49df --- /dev/null +++ b/apps/dashboard/src/lib/server/project-deletion.ts @@ -0,0 +1,84 @@ +import { error } from '@sveltejs/kit'; +import { eq, inArray } from 'drizzle-orm'; +import { getBackend } from '$lib/server/backends'; +import { + cancelProjectBilling, + deleteLocalProjectBillingCustomer, + deleteProjectServerEntity +} from '$lib/server/billing/autumn'; +import { meterResourceThrough, syncProjectUsage } from '$lib/server/billing/metering'; +import type { initDrizzle } from '$lib/server/db'; +import { + invitation, + ipAssignments, + member, + organization, + paymentPeriods, + volumes, + vms +} from '$lib/server/db/schema'; +import { releaseVmNetworking } from '$lib/server/ipam'; + +export async function deleteOrganizationResources( + db: ReturnType, + organizationId: string +) { + const projectVms = await db.query.vms.findMany({ + where: eq(vms.ownerProjectId, organizationId), + columns: { + id: true, + name: true, + proxmoxId: true, + active: true, + backend: true + } + }); + const vmIds = projectVms.map((vm) => vm.id); + + for (const vm of projectVms.filter((item) => item.active)) { + try { + await getBackend(vm.backend).deleteVm(vm.id, vm.proxmoxId ?? undefined); + } catch (err) { + console.warn(`Failed to deprovision VM ${vm.id} during project delete`, err); + error(502, `Failed to deprovision VM "${vm.name}" in Proxmox`); + } + } + + for (const vm of projectVms.filter((item) => item.active)) { + const metered = await meterResourceThrough('vm', vm.id).catch((err) => { + console.warn(`Failed to meter VM ${vm.id} during project delete`, err); + error(502, `Failed to meter VM "${vm.name}" during project delete`); + }); + if (!metered?.event || metered.syncStatus === 'synced') { + await deleteProjectServerEntity(organizationId, vm.id).catch((err) => { + console.warn(`Failed to delete Autumn entity for VM ${vm.id}`, err); + }); + } + } + await syncProjectUsage(organizationId); + + await db.delete(volumes).where(eq(volumes.ownerProjectId, organizationId)); + for (const vm of projectVms) { + await releaseVmNetworking(db, vm.id).catch((err) => { + console.warn(`Failed to release networking for VM ${vm.id} during project delete`, err); + }); + } + if (vmIds.length > 0) { + await db.delete(ipAssignments).where(inArray(ipAssignments.associatedVmId, vmIds)); + await db.delete(paymentPeriods).where(inArray(paymentPeriods.vmId, vmIds)); + } + await db + .update(vms) + .set({ active: false, deletedAt: Date.now(), ownerProjectId: null }) + .where(eq(vms.ownerProjectId, organizationId)); + await db.delete(invitation).where(eq(invitation.organizationId, organizationId)); + await db.delete(member).where(eq(member.organizationId, organizationId)); + const billingCancelled = await cancelProjectBilling(organizationId).catch((err) => { + console.warn(`Failed to cancel Autumn billing for project ${organizationId}`, err); + return false; + }); + if (billingCancelled) { + await deleteLocalProjectBillingCustomer(organizationId); + } + await db.delete(organization).where(eq(organization.id, organizationId)); +} diff --git a/apps/dashboard/src/lib/server/vm-deletion.ts b/apps/dashboard/src/lib/server/vm-deletion.ts index 8e38ba1..c86afac 100644 --- a/apps/dashboard/src/lib/server/vm-deletion.ts +++ b/apps/dashboard/src/lib/server/vm-deletion.ts @@ -1,7 +1,7 @@ import { getRequestEvent } from '$app/server'; -import { and, eq } from 'drizzle-orm'; +import { and, eq, inArray, isNotNull, lt } from 'drizzle-orm'; import { initDrizzle, closeRequestDb, type Database } from '$lib/server/db'; -import { vms } from '$lib/server/db/schema'; +import { ipAssignments, paymentPeriods, vms, volumes } from '$lib/server/db/schema'; import { getBackend } from '$lib/server/backends'; import { deleteProjectServerEntity } from '$lib/server/billing/autumn'; import { meterResourceThrough } from '$lib/server/billing/metering'; @@ -20,6 +20,31 @@ export async function queueVmDeletion(db: Database, row: DeletableVm): Promise row.id); + if (vmIds.length === 0) return { purged: 0 }; + + await db + .update(volumes) + .set({ associatedVmId: null }) + .where(inArray(volumes.associatedVmId, vmIds)); + await db.delete(ipAssignments).where(inArray(ipAssignments.associatedVmId, vmIds)); + await db.delete(paymentPeriods).where(inArray(paymentPeriods.vmId, vmIds)); + await db.delete(vms).where(inArray(vms.id, vmIds)); + + return { purged: vmIds.length }; +} + function initOwnedDb() { const event = getRequestEvent(); const ownsDb = !event.locals.db; @@ -58,7 +83,7 @@ async function deleteVmResources(row: DeletableVm): Promise { try { const claimed = await db .update(vms) - .set({ active: false }) + .set({ active: false, deletedAt: Date.now() }) .where(and(eq(vms.id, row.id), eq(vms.active, true))) .returning({ id: vms.id }); if (claimed.length === 0) return; diff --git a/apps/dashboard/src/lib/server/vm-provisioning.ts b/apps/dashboard/src/lib/server/vm-provisioning.ts new file mode 100644 index 0000000..6ef6b3d --- /dev/null +++ b/apps/dashboard/src/lib/server/vm-provisioning.ts @@ -0,0 +1,198 @@ +import { error } from '@sveltejs/kit'; +import { and, eq } from 'drizzle-orm'; +import { getRequestEvent } from '$app/server'; +import { runInBackground } from '$lib/server/background'; +import { getBackend } from '$lib/server/backends'; +import { + deleteProjectServerEntity, + ensureProjectServerEntity, + isBillingConfigured +} from '$lib/server/billing/autumn'; +import { createBillingMeter } from '$lib/server/billing/metering'; +import { closeRequestDb, initDrizzle, type Database } from '$lib/server/db'; +import { baseImages, vms, vmTypes } from '$lib/server/db/schema'; +import { allocateVmNetworking, generateMacAddress, releaseVmNetworking } from '$lib/server/ipam'; + +export type ProvisionVmInput = { + projectId: string; + vmTypeId: string; + name: string; + userId: string; + billingExempt: boolean; + networkingMode?: 'both' | 'ipv6'; + imageId?: string; + sshPublicKeys?: string[]; + password?: string; +}; + +export async function provisionVm(db: Database, input: ProvisionVmInput) { + const [vmType, baseImage] = await Promise.all([ + db.query.vmTypes.findFirst({ + where: eq(vmTypes.id, input.vmTypeId) + }), + input.imageId + ? db.query.baseImages.findFirst({ + where: eq(baseImages.id, input.imageId) + }) + : null + ]); + if (!vmType) error(400, `VM type "${input.vmTypeId}" not found`); + if (isBillingConfigured() && !vmType.autumnFeatureId) + error(400, `VM type "${vmType.name}" is missing an Autumn feature ID`); + const featureId = vmType.autumnFeatureId; + + if (input.imageId && !baseImage) error(400, `Image "${input.imageId}" not found`); + + const now = Date.now(); + const [inserted] = await db + .insert(vms) + .values({ + name: input.name, + proxmoxId: null, + active: true, + ownerProjectId: input.projectId, + vmTypeId: input.vmTypeId, + creationDate: new Date().toISOString().split('T')[0], + createdAt: now, + backend: 'proxmox', + status: 'provisioning' + }) + .returning(); + + const vmId = inserted.id; + const macAddress = generateMacAddress(); + let networkingAllocations: Awaited> = []; + let result; + try { + if (!input.billingExempt) { + await ensureProjectServerEntity({ + projectId: input.projectId, + serverId: vmId, + name: input.name + }); + } + networkingAllocations = await allocateVmNetworking(db, { + vmId, + macAddress, + mode: input.networkingMode ?? 'both' + }); + const ipv4NetworkAllocation = networkingAllocations.find( + (allocation) => allocation.family === 'ipv4' && allocation.address + ); + const ipv6TransitNetworkAllocation = networkingAllocations.find( + (allocation) => allocation.family === 'ipv6' && allocation.address + ); + const ipv6PrefixNetworkAllocation = networkingAllocations.find( + (allocation) => allocation.family === 'ipv6' && allocation.prefix + ); + const firewallIpSet = [ + ...(ipv4NetworkAllocation?.address ? [`${ipv4NetworkAllocation.address}/32`] : []), + ...(ipv6TransitNetworkAllocation?.address + ? [`${ipv6TransitNetworkAllocation.address}/128`] + : []), + ...(ipv6PrefixNetworkAllocation?.prefix ? [ipv6PrefixNetworkAllocation.prefix] : []) + ]; + const backend = getBackend('proxmox'); + result = await backend.createVm({ + id: vmId, + name: input.name, + proxmoxId: inserted.proxmoxId ?? undefined, + macAddress, + cores: vmType.cores, + memoryMb: vmType.ramCapacity, + diskGb: vmType.storageAmount, + imageId: input.imageId, + imageSource: baseImage?.filePath, + secureBoot: baseImage?.secureBoot ?? true, + networkConfig: { + firewallIpSet, + ...(ipv4NetworkAllocation?.address + ? { + ipv4: { + address: ipv4NetworkAllocation.address, + prefixLength: ipv4NetworkAllocation.prefixLength, + gateway: ipv4NetworkAllocation.sourcePrefix.gatewayAddress ?? '' + } + } + : {}), + ...(ipv6TransitNetworkAllocation?.address + ? { + ipv6: { + address: ipv6TransitNetworkAllocation.address, + prefixLength: ipv6TransitNetworkAllocation.prefixLength + } + } + : {}), + ...(ipv6PrefixNetworkAllocation?.prefix + ? { ipv6Prefix: ipv6PrefixNetworkAllocation.prefix } + : {}) + }, + sshKeys: input.sshPublicKeys ?? [], + password: input.password, + onProvisionSettled: async ({ ok, error: err }) => { + const settledEvent = getRequestEvent(); + const settledDb = initDrizzle(); + try { + await settledDb + .update(vms) + .set( + ok ? { status: 'ready' } : { status: 'error', statusError: err ?? 'Unknown error' } + ) + .where(and(eq(vms.id, vmId), eq(vms.active, true))); + console.log(`VM ${vmId} provision ${ok ? 'succeeded' : 'failed'}`); + } catch (updateErr) { + console.error(`VM ${vmId} status update failed:`, updateErr); + } finally { + closeRequestDb(settledEvent); + } + }, + registerBackground: runInBackground, + userId: input.userId, + projectId: input.projectId + }); + + if (!result.macAddress) error(502, 'Proxmox did not return a MAC address'); + } catch (err) { + if (result?.proxmoxId != null) { + await getBackend('proxmox') + .deleteVm(vmId, result.proxmoxId) + .catch((deleteErr) => { + console.warn(`Failed to clean up Proxmox VM ${vmId} after provisioning error`, deleteErr); + }); + } + await releaseVmNetworking(db, vmId).catch(() => {}); + await deleteProjectServerEntity(input.projectId, vmId).catch(() => {}); + await db + .delete(vms) + .where(eq(vms.id, inserted.id)) + .catch(() => {}); + throw err; + } + + const ipv4Allocation = networkingAllocations.find((allocation) => allocation.family === 'ipv4'); + const ipv6Allocation = networkingAllocations.find( + (allocation) => allocation.family === 'ipv6' && allocation.address + ); + + await db + .update(vms) + .set({ + proxmoxId: result.proxmoxId ?? null, + proxmoxNode: result.proxmoxNode ?? null, + lastKnownIpv4: ipv4Allocation?.address ?? null, + lastKnownIpv6: ipv6Allocation?.address ?? null + }) + .where(eq(vms.id, vmId)); + if (featureId) { + await createBillingMeter({ + projectId: input.projectId, + resourceType: 'vm', + resourceId: vmId, + featureId, + units: 1, + now + }); + } + + return { id: inserted.id, taskId: result.taskId }; +} diff --git a/apps/dashboard/src/lib/state/admin.svelte.ts b/apps/dashboard/src/lib/state/admin.svelte.ts index 7904874..58f2f96 100644 --- a/apps/dashboard/src/lib/state/admin.svelte.ts +++ b/apps/dashboard/src/lib/state/admin.svelte.ts @@ -37,6 +37,7 @@ import { listAllAdminVms, type AdminVm } from '$lib/remote/admin-vms.remote'; +import { setProjectBillingExempt, type AdminProject } from '$lib/remote/admin-projects.remote'; import { createVmType, deleteVmType, @@ -131,6 +132,7 @@ export type AdminPageData = { adminUsers?: AdminUser[]; ipamPrefixes?: IpamPrefix[]; adminVms?: AdminVm[]; + adminProjects?: AdminProject[]; }; export const colorOptions = [ @@ -158,6 +160,9 @@ export class AdminState { adminVms = $state([]); adminVmSaving = $state>({}); adminVmError = $state(''); + adminProjects = $state([]); + adminProjectSaving = $state>({}); + adminProjectError = $state(''); userSheetOpen = $state(false); selectedUser = $state(null); @@ -242,6 +247,7 @@ export class AdminState { this.ipamPrefixes = [...(data.ipamPrefixes ?? [])]; this.adminUsers = [...(data.adminUsers ?? [])]; this.adminVms = [...(data.adminVms ?? [])]; + this.adminProjects = [...(data.adminProjects ?? [])]; const incoming = data.featureFlags ?? { ...defaultFeatureFlags }; this.featureFlags = untrack(() => Object.fromEntries( @@ -338,6 +344,24 @@ export class AdminState { } } + async setProjectBillingExempt(projectId: string, billingExempt: boolean) { + const previousProjects = this.adminProjects.map((project) => ({ ...project })); + this.adminProjectError = ''; + this.adminProjectSaving[projectId] = true; + this.adminProjects = this.adminProjects.map((project) => + project.id === projectId ? { ...project, billingExempt } : project + ); + try { + await setProjectBillingExempt({ projectId, billingExempt }); + await invalidate('app:admin-projects'); + } catch (err) { + this.adminProjects = previousProjects; + this.adminProjectError = getErrorMessage(err, 'Failed to update billing exemption'); + } finally { + delete this.adminProjectSaving[projectId]; + } + } + async refreshAdminVms() { try { this.adminVms = await runQuery(listAllAdminVms()); diff --git a/apps/dashboard/src/routes/(app)/admin/+layout.server.ts b/apps/dashboard/src/routes/(app)/admin/+layout.server.ts index f6b6f25..f356330 100644 --- a/apps/dashboard/src/routes/(app)/admin/+layout.server.ts +++ b/apps/dashboard/src/routes/(app)/admin/+layout.server.ts @@ -4,6 +4,7 @@ import { listVmTypes } from '$lib/remote/vm-types.remote'; import { listImages } from '$lib/remote/images.remote'; import { listAdminUsers } from '$lib/remote/admin-users.remote'; import { listAllAdminVms } from '$lib/remote/admin-vms.remote'; +import { listAdminProjects } from '$lib/remote/admin-projects.remote'; import { listIpamPrefixes } from '$lib/remote/ipam.remote'; import { initDrizzle } from '$lib/server/db'; import { requireAdmin } from '$lib/server/auth-context'; @@ -14,6 +15,7 @@ export const load: LayoutServerLoad = async ({ depends }) => { depends('app:admin-users'); depends('app:ipam-prefixes'); depends('app:admin-vms'); + depends('app:admin-projects'); const event = getRequestEvent(); const userId = event?.locals.user?.id; @@ -23,20 +25,23 @@ export const load: LayoutServerLoad = async ({ depends }) => { images: [], adminUsers: [], ipamPrefixes: [], - adminVms: [] + adminVms: [], + adminProjects: [] }; } await requireAdmin(initDrizzle(), userId); - const [vmTypes, images, featureFlags, adminUsers, ipamPrefixes, adminVms] = await Promise.all([ - listVmTypes(), - listImages(), - getFeatureFlags(), - listAdminUsers(), - listIpamPrefixes(), - listAllAdminVms() - ]); + const [vmTypes, images, featureFlags, adminUsers, ipamPrefixes, adminVms, adminProjects] = + await Promise.all([ + listVmTypes(), + listImages(), + getFeatureFlags(), + listAdminUsers(), + listIpamPrefixes(), + listAllAdminVms(), + listAdminProjects() + ]); return { vmTypes, @@ -44,6 +49,7 @@ export const load: LayoutServerLoad = async ({ depends }) => { featureFlags, adminUsers, ipamPrefixes, - adminVms + adminVms, + adminProjects }; }; diff --git a/apps/dashboard/src/routes/(app)/admin/+layout.svelte b/apps/dashboard/src/routes/(app)/admin/+layout.svelte new file mode 100644 index 0000000..81be8be --- /dev/null +++ b/apps/dashboard/src/routes/(app)/admin/+layout.svelte @@ -0,0 +1,95 @@ + + +
+
+ {#each tabs as tab (tab.href)} + {@const active = + page.url.pathname === tab.href || page.url.pathname.startsWith(`${tab.href}/`)} + + + {tab.label} + {#if tab.count !== null} + {tab.count} + {/if} + + {/each} +
+ {@render children()} +
diff --git a/apps/dashboard/src/routes/(app)/admin/+page.server.ts b/apps/dashboard/src/routes/(app)/admin/+page.server.ts new file mode 100644 index 0000000..791c178 --- /dev/null +++ b/apps/dashboard/src/routes/(app)/admin/+page.server.ts @@ -0,0 +1,6 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async () => { + redirect(302, '/admin/users'); +}; diff --git a/apps/dashboard/src/routes/(app)/admin/+page.svelte b/apps/dashboard/src/routes/(app)/admin/+page.svelte deleted file mode 100644 index 382c43c..0000000 --- a/apps/dashboard/src/routes/(app)/admin/+page.svelte +++ /dev/null @@ -1,654 +0,0 @@ - - -
- - - - -
- {#if activeTab === 'features'} -
- {#if admin.featureFlagError} -
- {admin.featureFlagError} -
- {/if} -
- {#each featureFlagKeys as flag (flag)} - {@const enabled = admin.featureFlags[flag]} - {@const Icon = featureFlagIcons[flag] ?? Flag} -
-
- -
- {featureFlagLabels[flag]} -

Route visibility and direct access

-
-
-
- {#if admin.featureFlagSaving[flag]} - - {/if} -
- - -
-
-
- {#if flag !== featureFlagKeys[featureFlagKeys.length - 1]} -
- {/if} - {/each} -
-
- {:else if activeTab === 'vmTypes'} - {#if admin.vmTypes.length === 0} -
- -

No VM types yet

- -
- {:else} - - - - - - - - - - - - - - - {#each admin.vmTypes as vt, index (vt.id)} - vtDragOver(event, index)} - ondrop={(event) => vtDrop(event, index)} - > - - - - - - - - - - - - {/each} - -
NameISACoresRAMStorageRateCapAutumn featureActions
- vtDragStart(event, index)} - ondragend={vtDragEnd} - onkeydown={(event) => vtHandleKeydown(event, index)} - > - - - {vt.name}{vt.isa}{vt.cores}{vt.ramCapacity} MB{vt.storageAmount} GB${vt.rate}/hr${vt.cap}/mo - {vt.autumnFeatureId ?? 'Not configured'} - -
- - -
-
- {/if} - - - {:else if admin.images.length === 0} -
- -

No admin.images configured

- -
- {:else} - - - - - - - - - - - {#each admin.images as img (img.id)} - - - - - - - - - {/each} - -
ImageVersionISAProxmox PathDescriptionActions
-
- - - - {img.name} -
-
{img.version}{img.isa}{img.filePath}{img.description} -
- - -
-
- {/if} -
-
- - - - - - {admin.vtEditing ? 'Edit VM Type' : 'Create VM Type'} - {admin.vtEditing - ? 'Update plan specifications.' - : 'Define a new VM plan.'} - -
- {#if admin.vtError} -
- {admin.vtError} -
- {/if} -
- -
-
- - -
-
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
-
- - -
-
- - - - -
-
- - - - - - {admin.imgEditing ? 'Edit Image' : 'Add Image'} - Configure a base image for VM provisioning. - -
- {#if admin.imgError} -
- {admin.imgError} -
- {/if} - -
-
- -
-
- -
-
- -
-
- - -
-
- -
- - -
- - -
- -
- {#each colorOptions as c (c)} - - {/each} -
-
- - -
- - - {#if admin.imgIcon.trim()} -
- Preview: - {admin.imgIcon} -
- {/if} -
- - -
-
- - -
- - {#if admin.pveImages.length > 0} -
- {#each admin.pveImages as iso (iso.volid)} - - {/each} -
- {/if} -
-
- - - - -
-
diff --git a/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte b/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte index 05ec4ab..f065097 100644 --- a/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte +++ b/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte @@ -1,14 +1,10 @@ -
- -
- - - VM Types - {admin.vmTypes.length} - - - - VMs - - {admin.adminVms.filter((vm) => vm.active).length} - - - - - Images - {admin.images.length} - - - - Feature Flags - {enabledCount} - - - - IPAM - {admin.ipamPrefixes.length} - - - - Users - {userCount} - - - - Emails - -
- {#if activeTab === 'vmTypes'} -
- +
+
+ {#if admin.featureFlagError} +
+ {admin.featureFlagError}
- {:else if activeTab === 'images'} -
- +
+
+
+ {enabledCount} + of + {totalCount} + enabled +
- {/if} -
+
- -
- {#if activeTab === 'features'} -
- {#if admin.featureFlagError} + + {#each Object.entries(featureFlagCategories) as [category, flags] (category)} + {@const CatIcon = categoryIcons[category as FeatureFlagCategory]} + {@const catEnabled = flags.filter((f) => admin.featureFlags[f]).length} +
+
+
+ +

+ {featureFlagCategoryLabels[category as FeatureFlagCategory]} +

+
- {admin.featureFlagError} + {#if catEnabled === flags.length} + All on + {:else if catEnabled > 0} + {catEnabled}/{flags.length} on + {:else} + All off + {/if}
- {/if} - -
-
+
+
+ {#each flags as flag, i (flag)} + {@const enabled = admin.featureFlags[flag]} + {@const Icon = featureFlagIcons[flag]}
- {enabledCount} - of - {totalCount} - enabled -
-
-
- - - {#each Object.entries(featureFlagCategories) as [category, flags] (category)} - {@const CatIcon = categoryIcons[category as FeatureFlagCategory]} - {@const catEnabled = flags.filter((f) => admin.featureFlags[f]).length} -
-
-
- -

- {featureFlagCategoryLabels[category as FeatureFlagCategory]} -

-
-
- {#if catEnabled === flags.length} - All on - {:else if catEnabled > 0} - {catEnabled}/{flags.length} on - {:else} - All off - {/if} -
-
-
- {#each flags as flag, i (flag)} - {@const enabled = admin.featureFlags[flag]} - {@const Icon = featureFlagIcons[flag]} +
-
-
- -
-
-
- {featureFlagLabels[flag]} - {#if enabled} - - {/if} -
-

- {featureFlagDescriptions[flag]} -

-
-
-
- {#if admin.featureFlagSaving[flag]} - - {/if} -
+
+
+ {featureFlagLabels[flag]} + {#if enabled} - + class="hidden items-center gap-1 text-[10px] font-medium text-emerald-400 sm:flex" + >Active + {/if}
+

+ {featureFlagDescriptions[flag]} +

- {/each} -
-
- {/each} -
- {:else if activeTab === 'vmTypes'} - {#if admin.vmTypes.length === 0} -
- -

No VM types yet

- -
- {:else} - - - - - - - - - - - - - {#each admin.vmTypes as vt (vt.id)} - - - +
+ {#if admin.featureFlagSaving[flag]} + + {/if} +
- - - - - - - {/each} - -
NameISACoresRAMStorageRateCapActions
{vt.name}{vt.isa}{vt.cores}{vt.ramCapacity} MB{vt.storageAmount} GB${vt.rate}/hr${vt.cap}/mo -
- - -
-
- {/if} - - - {:else if admin.images.length === 0} -
- -

No admin.images configured

- -
- {:else} - - - - - - - - - - - {#each admin.images as img (img.id)} - - - - - - - - - {/each} - -
ImageVersionISAProxmox PathDescriptionActions
-
- - - {img.name} -
-
{img.version}{img.isa}{img.filePath}{img.description} -
- - -
-
- {/if} -
-
- - - - - - {admin.vtEditing ? 'Edit VM Type' : 'Create VM Type'} - {admin.vtEditing - ? 'Update plan specifications.' - : 'Define a new VM plan.'} - -
- {#if admin.vtError} -
- {admin.vtError} -
- {/if} -
- -
-
- - -
-
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
-
- - - - -
-
- - - - - - {admin.imgEditing ? 'Edit Image' : 'Add Image'} - Configure a base image for VM provisioning. - -
- {#if admin.imgError} -
- {admin.imgError} -
- {/if} - -
-
- -
-
- -
-
- -
-
- - -
-
- -
- - -
- - -
- -
- {#each colorOptions as c (c)} - + class="block aspect-square w-1/2 rounded-full bg-white shadow-xs ring-1 ring-black/5 transition-transform duration-200 {enabled + ? 'translate-x-full' + : 'translate-x-0'}" + > + +
+
{/each}
- - -
- - - {#if admin.imgIcon.trim()} -
- Preview: - {admin.imgIcon} -
- {/if} -
- - -
-
- - -
- - {#if admin.pveImages.length > 0} -
- {#each admin.pveImages as iso (iso.volid)} - - {/each} -
- {/if} -
-
- - - - - - + {/each} +
+
diff --git a/apps/dashboard/src/routes/(app)/admin/images/+page.svelte b/apps/dashboard/src/routes/(app)/admin/images/+page.svelte index 768e402..0eafee9 100644 --- a/apps/dashboard/src/routes/(app)/admin/images/+page.svelte +++ b/apps/dashboard/src/routes/(app)/admin/images/+page.svelte @@ -1,42 +1,23 @@ -
- -
- - - VM Types - {admin.vmTypes.length} - - - - VMs - - {admin.adminVms.filter((vm) => vm.active).length} - - - - - Images - {admin.images.length} - - - - Feature Flags - - {featureFlagKeys.filter((key) => admin.featureFlags[key]).length} - - - - - IPAM - {admin.ipamPrefixes.length} - - - - Users - {userCount} - - +
+ -
- {:else if activeTab === 'images'} -
- - -
- {/if} + Upload/Import Image + +
- - -
- {#if activeTab === 'features'} -
- {#if admin.featureFlagError} -
+ +

No admin.images configured

+ +
+ {:else} + + + + + + + - {admin.featureFlagError} - - {/if} -
- {#each featureFlagKeys as flag (flag)} - {@const enabled = admin.featureFlags[flag]} - {@const Icon = featureFlagIcons[flag] ?? Flag} -
-
- -
- {featureFlagLabels[flag]} -

Route visibility and direct access

-
-
-
- {#if admin.featureFlagSaving[flag]} - - {/if} -
- - -
-
-
- {#if flag !== featureFlagKeys[featureFlagKeys.length - 1]} -
- {/if} - {/each} -
- - {:else if activeTab === 'vmTypes'} - {#if admin.vmTypes.length === 0} -
- -

No VM types yet

- Description +
+ + + {#each admin.images as img, index (img.id)} + imgDragOver(event, index)} + ondrop={(event) => imgDrop(event, index)} > - - {:else} -
ImageVersionTypeProxmox PathActions
- - - - - - - - - + imgDragStart(event, index)} + ondragend={imgDragEnd} + onkeydown={(event) => imgHandleKeydown(event, index)} > - - - {#each admin.vmTypes as vt (vt.id)} - - - + + + - - - - - - - {/each} - -
NameISACoresRAMStorageRateCapActions
{vt.name}{vt.isa} +
+ -
{vt.cores}{vt.ramCapacity} MB{vt.storageAmount} GB${vt.rate}/hr${vt.cap}/mo -
- - + {:else} + + {/if} + +
+ {img.name} + {#if img.isOfficial} + Official -
-
- {/if} - - - {:else if admin.images.length === 0} -
- -

No admin.images configured

- -
- {:else} - - - - - - - + + + + - {img.filePath} - - - - {#each admin.images as img, index (img.id)} - imgDragOver(event, index)} - ondrop={(event) => imgDrop(event, index)} + - - - - - - - - - {/each} - -
ImageVersionTypeProxmox Path{img.version}{img.imageType || 'import'}DescriptionActions
{img.description} - imgDragStart(event, index)} - ondragend={imgDragEnd} - onkeydown={(event) => imgHandleKeydown(event, index)} + +
+ - - -
-
- - {#if img.isOfficial && img.logoSvg} - - {:else} - - {/if} - -
- {img.name} - {#if img.isOfficial} - Official - {/if} -
-
-
{img.version}{img.imageType || 'import'}{img.filePath}{img.description} -
- - -
-
- {/if} -
+ +
+ + + {/each} + + + {/if}
- - - - - {admin.vtEditing ? 'Edit VM Type' : 'Create VM Type'} - {admin.vtEditing - ? 'Update plan specifications.' - : 'Define a new VM plan.'} - -
- {#if admin.vtError} -
- {admin.vtError} -
- {/if} -
- -
-
- - -
-
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
-
- - - - -
-
- diff --git a/apps/dashboard/src/routes/(app)/admin/ipam/+page.svelte b/apps/dashboard/src/routes/(app)/admin/ipam/+page.svelte index c7135f0..e21f2e9 100644 --- a/apps/dashboard/src/routes/(app)/admin/ipam/+page.svelte +++ b/apps/dashboard/src/routes/(app)/admin/ipam/+page.svelte @@ -1,7 +1,6 @@ -
-
- - - VM Types - {admin.vmTypes.length} - - - - VMs - - {admin.adminVms.filter((vm) => vm.active).length} - - - - - Images - {admin.images.length} - - - - Feature Flags - {enabledCount} - - - - IPAM - {admin.ipamPrefixes.length} - - - - Users - {userCount} - - - - Emails - -
-
+
+
+
+ IPv4 + {ipv4Count} +
+
+ IPv6 + {ipv6Count} +
+
-
-
-
- IPv4 - {ipv4Count} -
-
- IPv6 - {ipv6Count} -
+ {#if admin.ipamPrefixes.length === 0} +
+ +

No IPAM prefixes configured

+
+ {:else} + + + + + + + + + + + + + + {#each admin.ipamPrefixes as prefix (prefix.id)} + + + + + + + - {#if admin.ipamPrefixes.length === 0} -
- -

No IPAM prefixes configured

- -
- {:else} -
NamePrefixFamilyGatewayModeAvailableActions
+
+ {prefix.name} + {#if prefix.disabled} + Disabled + {/if} +
+
{prefix.cidr} + {prefix.family} + + {prefix.gatewayAddress ?? '-'} + + + {prefix.family === 'ipv6' + ? prefix.ipv6UseTransitAddress + ? '/128 transit' + : '/64 prefixes' + : '/32 addresses'} + + + {formatCount(prefix.available)} + / {formatCount(prefix.capacity)} +
- - - - - - - - - + - - - {#each admin.ipamPrefixes as prefix (prefix.id)} - - - - - - - - - - - {/each} - -
NamePrefixFamilyGatewayModeAvailableActions +
+ + + +
+
-
- {prefix.name} - {#if prefix.disabled} - Disabled - {/if} -
-
{prefix.cidr} - {prefix.family} - - {prefix.gatewayAddress ?? '—'} - - - {prefix.family === 'ipv6' - ? prefix.ipv6UseTransitAddress - ? '/128 transit' - : '/64 prefixes' - : '/32 addresses'} - - - {formatCount(prefix.available)} - / {formatCount(prefix.capacity)} - -
- - - -
-
- {/if} -
+ {/each} + + + {/if}
diff --git a/apps/dashboard/src/routes/(app)/admin/projects/[[id]]/+page.svelte b/apps/dashboard/src/routes/(app)/admin/projects/[[id]]/+page.svelte new file mode 100644 index 0000000..0a59809 --- /dev/null +++ b/apps/dashboard/src/routes/(app)/admin/projects/[[id]]/+page.svelte @@ -0,0 +1,1415 @@ + + + + Projects + + +{#snippet projectDetail(project: AdminProject)} + {@const vmsForProject = projectVms(project.id)} + {@const billing = billingInfo(project)} +
+
+
+ {project.name} + {project.slug} +
+
+ {#if project.disabled} + + disabled + + {/if} + {#if project.billingExempt} + + exempt + + {/if} + + {billing.label} + +
+
+ +
+
+ {project.memberCount} + Members +
+
+ {project.vmCount} + VMs +
+
+ {project.volumeCount} + Volumes +
+
+ +
+ Owner +
+
+ +
+ {#if project.ownerId} + + {project.ownerName ?? 'No owner'} + + {:else} + No owner + {/if} + {project.ownerEmail ?? '-'} +
+
+ {#if project.ownerBillingExempt} + + Billing exempt + + {/if} +
+ {#if project.ownerId} + {@const ownerId = project.ownerId} +
+
+ +
+ Owner billing exempt + + {project.ownerBillingExempt + ? 'No usage is billed in any project they own' + : 'Usage is metered and billed normally'} + +
+
+
+ {#if admin.userSheetSaving[ownerId]?.field === 'billingExempt' && admin.userSheetSaving[ownerId]?.saving} + + {/if} + project.ownerBillingExempt, (v) => admin.setUserBillingExempt(ownerId, v) + } + /> +
+
+ {/if} +
+ + + +
+ Billing +
+
+ +
+ Project billing exempt + + {project.billingExempt + ? 'Usage in this project is not metered or billed' + : 'Usage in this project is metered and billed normally'} + +
+
+
+ {#if admin.adminProjectSaving[project.id]} + + {/if} + project.billingExempt, (v) => admin.setProjectBillingExempt(project.id, v) + } + /> +
+
+
+ + + +
+
+ Virtual machines + +
+ {#if vmsForProject.length === 0} +

No active VMs in this project

+ {:else} +
+ {#each vmsForProject as vm (vm.id)} + {@const info = vmStatusInfo(vm)} + {@const saving = admin.adminVmSaving[vm.id]} +
+
+ + {vm.name} + + + {vm.vmTypeName ?? '-'} · {vm.lastKnownIpv4 ?? vm.lastKnownIpv6 ?? vm.id} + +
+
+ + {info.label} + + + + + + + admin.adminVmPower(vm.id, 'start')} + > + Start + + admin.adminVmPower(vm.id, 'stop')} + > + Shut down + + admin.adminVmPower(vm.id, 'reboot')} + > + Reboot + + admin.adminVmPower(vm.id, 'kill')} + > + Force stop + + + openReversal(vm)} + > + Reverse billing + + + deleteVm(vm)} + > + Delete + + + +
+
+ {/each} +
+ {/if} +
+ + + +
+ Details +
+ + Project ID + + {project.id} +
+
+ + Created + + {formatDate(project.createdAt)} +
+
+ + + +
+
+ +
+ Danger zone + + Disabling blocks all member access and stops new provisioning. Deleting deprovisions + every VM and volume and cancels billing. + +
+
+
+ + +
+
+
+{/snippet} + +
+
+ {#if admin.adminVmError || admin.adminProjectError} +
+ + {admin.adminVmError || admin.adminProjectError} +
+ {/if} + +
+
+

Projects

+

+ Manage user projects, their servers, and billing state. +

+
+ +
+ +
+
+
+ +
+
+ {admin.adminProjects.length} + Total projects +
+
+
+
+ +
+
+ {withVmsCount} + With VMs +
+
+
+
+ +
+
+ {billingConfiguredCount} + Billing configured +
+
+
+
+ +
+
+ {attentionCount} + Needs attention +
+
+
+ +
+
+ + +
+ + + + + + (ownerFilter = 'all')} + > + All owners + {#if ownerFilter === 'all'}{/if} + + + {#each ownerOptions as owner (owner.email)} + (ownerFilter = owner.email)} + > + + {owner.name} + {owner.email} + + {#if ownerFilter === owner.email} + + {/if} + + {/each} + + + + + + + + (billingFilter = 'all')} + > + All billing states + {#if billingFilter === 'all'}{/if} + + + {#each billingFilterOptions as option (option.value)} + (billingFilter = option.value)} + > + {option.label} + {#if billingFilter === option.value} + + {/if} + + {/each} + + +
+ + {#if filteredProjects.length === 0} +
+ +

No projects found

+
+ {:else} +
+ + + + + + + + + + + + + + {#each filteredProjects as project (project.id)} + {@const billing = billingInfo(project)} + openProjectSheet(project)} + > + + + + + + + + + {/each} + +
ProjectOwnerMembersResourcesBillingCreated
+
+ {project.name} + {project.slug} +
+
+
+ {project.ownerName ?? '-'} + + {project.ownerEmail ?? 'no owner'} + {#if project.ownerBillingExempt} + + No billing + + {/if} + +
+
{project.memberCount} +
+ + {project.vmCount} + + + {project.volumeCount} + +
+
+
+ {#if project.disabled} + + disabled + + {/if} + {#if project.billingExempt} + + exempt + + {/if} + + {billing.label} + +
+
{formatDate(project.createdAt)} + +
+
+ {/if} +
+
+ + !value && closeProjectSheet()}> + + {#if selectedProject} + {@render projectDetail(selectedProject)} + {/if} + + + + + + + + + Create project + + Creates a project owned by the selected user, with billing set up like a normal signup. + + + +
{ + event.preventDefault(); + void submitCreateProject(); + }} + > +
+ + +
+ +
+ + +
+ {#each createProjectOwners as owner (owner.id)} + + {:else} +

No users found

+ {/each} +
+
+ + {#if createProjectError} +

{createProjectError}

+ {/if} + + + + + +
+
+
+ + + + + + Create server in {createVmProject?.name ?? 'project'} + + + Provisions a server in this project. Usage is billed to the project unless it is exempt. + + + +
{ + event.preventDefault(); + void submitCreateVm(); + }} + > +
+ + +
+ +
+
+ + + + + + + {#each admin.vmTypes as vmType (vmType.id)} + (createVmTypeId = vmType.id)} + > + + {vmType.name} + + {vmType.cores}c · {vmType.ramCapacity} MB · {vmType.storageAmount} GB + + + {#if createVmTypeId === vmType.id} + + {/if} + + {/each} + + +
+
+ + + + + + + (createVmImageId = '')} + > + No image + {#if !createVmImageId}{/if} + + + {#each admin.images as image (image.id)} + (createVmImageId = image.id)} + > + {image.name} {image.version} + {#if createVmImageId === image.id} + + {/if} + + {/each} + + +
+
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ + {#if createVmOwnerKeysLoading} +

+ Loading keys... +

+ {:else if createVmOwnerKeys.length === 0} +

The project owner has no SSH keys.

+ {:else} +
+ {#each createVmOwnerKeys as key (key.id)} + + {/each} +
+ {/if} +
+ + {#if createVmError} +

{createVmError}

+ {/if} + + + + + +
+
+
+ + { + if (!value && !deleteVerifying) resetDeleteDialog(); + }} +> + + + Delete {deleteProjectName}? + + {deleteVerificationDescription} + + + +
{ + event.preventDefault(); + void confirmDeleteProject(); + }} + > +
+

This action cannot be undone.

+

+ Deleting {deleteProjectName} deprovisions every server, deletes volumes and invitations, records + final usage, and cancels billing for this project. +

+
+ + {#if deleteVerificationMethod === 'passkey'} +
+ +

+ Your browser will ask you to authenticate with your registered passkey. +

+
+ {:else} +
+ + + {#if deleteVerificationMethod === 'email'} +

+ We sent a deletion code to {deleteVerificationEmail}. +

+ {/if} +
+ {/if} + + {#if deleteError} +

{deleteError}

+ {/if} + + + + + +
+
+
diff --git a/apps/dashboard/src/routes/(app)/admin/users/+page.svelte b/apps/dashboard/src/routes/(app)/admin/users/[[id]]/+page.svelte similarity index 72% rename from apps/dashboard/src/routes/(app)/admin/users/+page.svelte rename to apps/dashboard/src/routes/(app)/admin/users/[[id]]/+page.svelte index 7e01c0e..7bfc141 100644 --- a/apps/dashboard/src/routes/(app)/admin/users/+page.svelte +++ b/apps/dashboard/src/routes/(app)/admin/users/[[id]]/+page.svelte @@ -1,6 +1,8 @@ + +
+
+ +
+ {#if admin.vmTypes.length === 0} +
+ +

No VM types yet

+ +
+ {:else} + + + + + + + + + + + + + + + {#each admin.vmTypes as vt, index (vt.id)} + vtDragOver(event, index)} + ondrop={(event) => vtDrop(event, index)} + > + + + + + + + + + + + + {/each} + +
NameISACoresRAMStorageRateCapAutumn featureActions
+ vtDragStart(event, index)} + ondragend={vtDragEnd} + onkeydown={(event) => vtHandleKeydown(event, index)} + > + + + {vt.name}{vt.isa}{vt.cores}{vt.ramCapacity} MB{vt.storageAmount} GB${vt.rate}/hr${vt.cap}/mo + {vt.autumnFeatureId ?? 'Not configured'} + +
+ + +
+
+ {/if} +
+ + + + + + {admin.vtEditing ? 'Edit VM Type' : 'Create VM Type'} + {admin.vtEditing + ? 'Update plan specifications.' + : 'Define a new VM plan.'} + +
+ {#if admin.vtError} +
+ {admin.vtError} +
+ {/if} +
+ +
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ + +
+
+ + + + +
+
diff --git a/apps/dashboard/src/routes/(app)/admin/vms/+page.svelte b/apps/dashboard/src/routes/(app)/admin/vms/+page.svelte deleted file mode 100644 index 3dac8c7..0000000 --- a/apps/dashboard/src/routes/(app)/admin/vms/+page.svelte +++ /dev/null @@ -1,662 +0,0 @@ - - - - VMs - - -
- - - - -
-
- {#if admin.adminVmError} -
- - {admin.adminVmError} -
- {/if} - - -
-
-
- -
-
- {activeVms.length} - Total VMs -
-
-
-
- -
-
- {runningCount} - Running -
-
-
-
- -
-
- {stoppedCount} - Stopped -
-
-
-
- -
-
- {errorCount} - Errors -
-
-
- - -
-
- - -
- - - - - - (ownerFilter = 'all')} - > - All owners - {#if ownerFilter === 'all'}{/if} - - - {#each ownerOptions as owner (owner.email)} - (ownerFilter = owner.email)} - > - - {owner.name} - {owner.email} - - {#if ownerFilter === owner.email} - - {/if} - - {/each} - - - - - - - - (typeFilter = 'all')} - > - All types - {#if typeFilter === 'all'}{/if} - - - {#each typeOptions as vmTypeName (vmTypeName)} - (typeFilter = vmTypeName)} - > - {vmTypeName} - {#if typeFilter === vmTypeName} - - {/if} - - {/each} - - - - - - - - (statusFilter = 'all')} - > - All statuses - {#if statusFilter === 'all'}{/if} - - - {#each statusOptions as status (status)} - (statusFilter = status)} - > - {status} - {#if statusFilter === status} - - {/if} - - {/each} - - - - -
- - {#if filteredVms.length === 0} -
- -

No servers found

-
- {:else} -
- - - - - - - - - - - - - - {#each filteredVms as vm (vm.id)} - {@const info = statusInfo(vm)} - {@const saving = admin.adminVmSaving[vm.id]} - - - - - - - - - - {/each} - -
ServerProject / OwnerTypeStatusUptimeCreated
-
- {vm.name} - - {vm.lastKnownIpv4 ?? vm.lastKnownIpv6 ?? vm.id} - -
-
-
- {vm.projectName ?? '—'} - - {vm.ownerEmail ?? 'no owner'} - {#if vm.ownerBillingExempt} - - No billing - - {/if} - -
-
-
- {vm.vmTypeName ?? '—'} - {#if vm.vmTypeCores} - - {vm.vmTypeCores}c · {vm.vmTypeRamCapacity} MB · {vm.vmTypeStorageAmount} GB - - {/if} -
-
- - {info.label} - - - {vm.liveStatus === 'running' ? formatUptime(vm.uptime) : '—'} - {formatDate(vm.createdAt)} - {#if vm.active} - - - - - - admin.adminVmPower(vm.id, 'start')} - > - Start - - admin.adminVmPower(vm.id, 'stop')} - > - Shut down - - admin.adminVmPower(vm.id, 'reboot')} - > - Reboot - - admin.adminVmPower(vm.id, 'kill')} - > - Force stop - - - openDeleteDialog(vm)} - > - Delete - - - - {/if} -
-
- {/if} -
-
-
- - - { - if (!value) closeDeleteDialog(); - }} -> - - - Delete {deleteTarget?.name}? - - This deprovisions the server in Proxmox, releases its networking, and records final usage. - It belongs to {deleteTarget?.projectName ?? 'an unknown project'} - ({deleteTarget?.ownerEmail ?? 'no owner'}). - - - -
{ - event.preventDefault(); - void confirmDelete(); - }} - > -
-

This action cannot be undone.

-

Type {deleteTarget?.name} to confirm.

-
- - - - - - - -
-
-
diff --git a/apps/dashboard/src/routes/(app)/admin/vms/[[id]]/+page.svelte b/apps/dashboard/src/routes/(app)/admin/vms/[[id]]/+page.svelte new file mode 100644 index 0000000..35558a4 --- /dev/null +++ b/apps/dashboard/src/routes/(app)/admin/vms/[[id]]/+page.svelte @@ -0,0 +1,610 @@ + + + + VMs + + +
+
+ {#if admin.adminVmError} +
+ + {admin.adminVmError} +
+ {/if} + + +
+
+
+ +
+
+ {activeVms.length} + Total VMs +
+
+
+
+ +
+
+ {runningCount} + Running +
+
+
+
+ +
+
+ {stoppedCount} + Stopped +
+
+
+
+ +
+
+ {errorCount} + Errors +
+
+
+ + +
+
+ + +
+ + + + + + (ownerFilter = 'all')} + > + All owners + {#if ownerFilter === 'all'}{/if} + + + {#each ownerOptions as owner (owner.email)} + (ownerFilter = owner.email)} + > + + {owner.name} + {owner.email} + + {#if ownerFilter === owner.email} + + {/if} + + {/each} + + + + + + + + (typeFilter = 'all')} + > + All types + {#if typeFilter === 'all'}{/if} + + + {#each typeOptions as vmTypeName (vmTypeName)} + (typeFilter = vmTypeName)} + > + {vmTypeName} + {#if typeFilter === vmTypeName} + + {/if} + + {/each} + + + + + + + + (statusFilter = 'all')} + > + All statuses + {#if statusFilter === 'all'}{/if} + + + {#each statusOptions as status (status)} + (statusFilter = status)} + > + {status} + {#if statusFilter === status} + + {/if} + + {/each} + + + + +
+ + {#if filteredVms.length === 0} +
+ +

No servers found

+
+ {:else} +
+ + + + + + + + + + + + + + {#each filteredVms as vm (vm.id)} + {@const info = statusInfo(vm)} + {@const saving = admin.adminVmSaving[vm.id]} + openVmSheet(vm)} + > + + + + + + + + + {/each} + +
ServerProject / OwnerTypeStatusUptimeCreated
+
+ {vm.name} + + {vm.lastKnownIpv4 ?? vm.lastKnownIpv6 ?? vm.id} + +
+
+
+ {vm.projectName ?? '-'} + + {vm.ownerEmail ?? 'no owner'} + {#if vm.ownerBillingExempt || vm.projectBillingExempt} + + No billing + + {/if} + +
+
+
+ {vm.vmTypeName ?? '-'} + {#if vm.vmTypeCores} + + {vm.vmTypeCores}c · {vm.vmTypeRamCapacity} MB · {vm.vmTypeStorageAmount} GB + + {/if} +
+
+ + {info.label} + + + {vm.liveStatus === 'running' ? formatUptime(vm.uptime) : '-'} + {formatDate(vm.createdAt)} event.stopPropagation()}> + + + + + + {#if vm.active} + admin.adminVmPower(vm.id, 'start')} + > + Start + + admin.adminVmPower(vm.id, 'stop')} + > + Shut down + + admin.adminVmPower(vm.id, 'reboot')} + > + Reboot + + admin.adminVmPower(vm.id, 'kill')} + > + Force stop + + + {/if} + openReversal(vm)} + > + Reverse billing + + {#if vm.active} + + openDeleteDialog(vm)} + > + Delete + + {/if} + + +
+
+ {/if} +
+
+ + + { + if (!value) closeDeleteDialog(); + }} +> + + + Delete {deleteTarget?.name}? + + This deprovisions the server in Proxmox, releases its networking, and records final usage. + It belongs to {deleteTarget?.projectName ?? 'an unknown project'} + ({deleteTarget?.ownerEmail ?? 'no owner'}). + + + +
{ + event.preventDefault(); + void confirmDelete(); + }} + > +
+

This action cannot be undone.

+

Type {deleteTarget?.name} to confirm.

+
+ + + + + + + +
+
+
+ + + + diff --git a/apps/dashboard/src/routes/(app)/projects/[projectid]/billing/+page.svelte b/apps/dashboard/src/routes/(app)/projects/[projectid]/billing/+page.svelte index 3eb8506..bc75e9f 100644 --- a/apps/dashboard/src/routes/(app)/projects/[projectid]/billing/+page.svelte +++ b/apps/dashboard/src/routes/(app)/projects/[projectid]/billing/+page.svelte @@ -227,7 +227,7 @@

- {hasCost ? formatCost(totalCost) : '—'} + {hasCost ? formatCost(totalCost) : '-'}

diff --git a/apps/dashboard/src/routes/(app)/projects/[projectid]/colocation/[id]/+page.svelte b/apps/dashboard/src/routes/(app)/projects/[projectid]/colocation/[id]/+page.svelte index 37b6238..e04c9f4 100644 --- a/apps/dashboard/src/routes/(app)/projects/[projectid]/colocation/[id]/+page.svelte +++ b/apps/dashboard/src/routes/(app)/projects/[projectid]/colocation/[id]/+page.svelte @@ -111,7 +111,7 @@
Location Chicago, IL — {colo.selectedUnit.location}Chicago, IL · {colo.selectedUnit.location}
diff --git a/apps/dashboard/src/routes/(app)/projects/[projectid]/firewall/+page.svelte b/apps/dashboard/src/routes/(app)/projects/[projectid]/firewall/+page.svelte index e41083a..8da4571 100644 --- a/apps/dashboard/src/routes/(app)/projects/[projectid]/firewall/+page.svelte +++ b/apps/dashboard/src/routes/(app)/projects/[projectid]/firewall/+page.svelte @@ -182,7 +182,7 @@
- Preview — firewall isn't live yet. Changes here aren't saved. + Preview: firewall isn't live yet. Changes here aren't saved.
diff --git a/apps/dashboard/src/routes/(app)/projects/[projectid]/networking/+page.svelte b/apps/dashboard/src/routes/(app)/projects/[projectid]/networking/+page.svelte index 8db52d8..d9cfdd3 100644 --- a/apps/dashboard/src/routes/(app)/projects/[projectid]/networking/+page.svelte +++ b/apps/dashboard/src/routes/(app)/projects/[projectid]/networking/+page.svelte @@ -118,7 +118,7 @@
- Preview — networking isn't live yet. Changes here aren't saved. + Preview: networking isn't live yet. Changes here aren't saved.
@@ -179,7 +179,7 @@
{:else} - {ip.rdns || '—'} + {ip.rdns || '-'} {/if} diff --git a/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/+layout.svelte b/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/+layout.svelte index 102f4af..5630144 100644 --- a/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/+layout.svelte +++ b/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/+layout.svelte @@ -14,7 +14,7 @@ let { data, children } = $props(); function formatUptime(seconds: number): string { - if (!seconds) return '—'; + if (!seconds) return '-'; const d = Math.floor(seconds / 86400); const h = Math.floor((seconds % 86400) / 3600); const m = Math.floor((seconds % 3600) / 60); diff --git a/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/[id]/+page.svelte b/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/[id]/+page.svelte index 85f0009..b3850dc 100644 --- a/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/[id]/+page.svelte +++ b/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/[id]/+page.svelte @@ -26,12 +26,12 @@ let lastSampleKeyByServer = $state>({}); function formatPercent(value: number | null) { - if (value == null) return '—'; + if (value == null) return '-'; return `${Math.round(value * 100)}%`; } function formatRate(bytes: number | null) { - if (bytes == null) return '—'; + if (bytes == null) return '-'; const units = ['B/s', 'KB/s', 'MB/s', 'GB/s']; let value = bytes; let unit = 0; diff --git a/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/[id]/backups/+page.svelte b/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/[id]/backups/+page.svelte index 5810058..a19e313 100644 --- a/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/[id]/backups/+page.svelte +++ b/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/[id]/backups/+page.svelte @@ -18,7 +18,7 @@
- Preview — backups aren't available yet. The entries below are sample data. + Preview: backups aren't available yet. The entries below are sample data.
diff --git a/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/[id]/logs/+page.svelte b/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/[id]/logs/+page.svelte index 1755535..bc67ed3 100644 --- a/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/[id]/logs/+page.svelte +++ b/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/[id]/logs/+page.svelte @@ -124,7 +124,7 @@
- Sample log data — live log streaming isn't available yet. + Sample log data. Live log streaming isn't available yet.
{#if selectedServer.status !== 'running'} diff --git a/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/[id]/networking/+page.svelte b/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/[id]/networking/+page.svelte index 7def514..0a91985 100644 --- a/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/[id]/networking/+page.svelte +++ b/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/[id]/networking/+page.svelte @@ -74,7 +74,7 @@ onclick={() => (editingRdnsKey = null)}>
{:else}
- {ipv4Rdns || '—'}