mirror of
https://github.com/apotdevin/thunderhub.git
synced 2026-08-13 12:33:08 +02:00
feat: node tables
This commit is contained in:
parent
e3f882b329
commit
129d094156
16 changed files with 942 additions and 1 deletions
1
.env
1
.env
|
|
@ -71,6 +71,7 @@
|
|||
# DB_TYPE='sqlite' # 'sqlite' or 'postgres' (omit to disable database)
|
||||
# DB_SQLITE_PATH='/path/to/thunderhub.db' # Required when DB_TYPE is 'sqlite'
|
||||
# DB_POSTGRES_URL='postgres://user:pass@localhost:5432/thunderhub' # Required when DB_TYPE is 'postgres'
|
||||
# DB_ENCRYPTION_KEY='' # 64-char hex string (32 bytes) for AES-256-GCM encryption of node credentials at rest. Generate with: openssl rand -hex 32
|
||||
|
||||
# -----------
|
||||
# SSL Config
|
||||
|
|
|
|||
25
drizzle/pg/0001_jazzy_ezekiel.sql
Normal file
25
drizzle/pg/0001_jazzy_ezekiel.sql
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
CREATE TABLE "nodes" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"team_id" uuid NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"type" text NOT NULL,
|
||||
"network" text NOT NULL,
|
||||
"socket" text NOT NULL,
|
||||
"encrypted_macaroon" text,
|
||||
"encrypted_cert" text,
|
||||
"created_at" timestamp(6) DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp(6) DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "user_nodes" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"node_id" uuid NOT NULL,
|
||||
"created_at" timestamp(6) DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp(6) DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "user_nodes_user_id_node_id_unique" UNIQUE("user_id","node_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "nodes" ADD CONSTRAINT "nodes_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "user_nodes" ADD CONSTRAINT "user_nodes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "user_nodes" ADD CONSTRAINT "user_nodes_node_id_nodes_id_fk" FOREIGN KEY ("node_id") REFERENCES "public"."nodes"("id") ON DELETE no action ON UPDATE no action;
|
||||
312
drizzle/pg/meta/0001_snapshot.json
Normal file
312
drizzle/pg/meta/0001_snapshot.json
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
{
|
||||
"id": "db15d19a-e7aa-485a-aa86-83712b251358",
|
||||
"prevId": "f2a5cebb-ac67-464a-bfc5-d0fa612c3552",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.nodes": {
|
||||
"name": "nodes",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"team_id": {
|
||||
"name": "team_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"network": {
|
||||
"name": "network",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"socket": {
|
||||
"name": "socket",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"encrypted_macaroon": {
|
||||
"name": "encrypted_macaroon",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"encrypted_cert": {
|
||||
"name": "encrypted_cert",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"nodes_team_id_teams_id_fk": {
|
||||
"name": "nodes_team_id_teams_id_fk",
|
||||
"tableFrom": "nodes",
|
||||
"tableTo": "teams",
|
||||
"columnsFrom": [
|
||||
"team_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.teams": {
|
||||
"name": "teams",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user_nodes": {
|
||||
"name": "user_nodes",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"node_id": {
|
||||
"name": "node_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_nodes_user_id_users_id_fk": {
|
||||
"name": "user_nodes_user_id_users_id_fk",
|
||||
"tableFrom": "user_nodes",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"user_nodes_node_id_nodes_id_fk": {
|
||||
"name": "user_nodes_node_id_nodes_id_fk",
|
||||
"tableFrom": "user_nodes",
|
||||
"tableTo": "nodes",
|
||||
"columnsFrom": [
|
||||
"node_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_nodes_user_id_node_id_unique": {
|
||||
"name": "user_nodes_user_id_node_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"user_id",
|
||||
"node_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'member'"
|
||||
},
|
||||
"team_id": {
|
||||
"name": "team_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"users_team_id_teams_id_fk": {
|
||||
"name": "users_team_id_teams_id_fk",
|
||||
"tableFrom": "users",
|
||||
"tableTo": "teams",
|
||||
"columnsFrom": [
|
||||
"team_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,13 @@
|
|||
"when": 1775074089419,
|
||||
"tag": "0000_new_adam_warlock",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1775160645885,
|
||||
"tag": "0001_jazzy_ezekiel",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
25
drizzle/sqlite/0001_fixed_nightshade.sql
Normal file
25
drizzle/sqlite/0001_fixed_nightshade.sql
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
CREATE TABLE `nodes` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`team_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`network` text NOT NULL,
|
||||
`socket` text NOT NULL,
|
||||
`encrypted_macaroon` text,
|
||||
`encrypted_cert` text,
|
||||
`created_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
|
||||
`updated_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
|
||||
FOREIGN KEY (`team_id`) REFERENCES `teams`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `user_nodes` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`node_id` text NOT NULL,
|
||||
`created_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
|
||||
`updated_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`node_id`) REFERENCES `nodes`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `user_nodes_user_id_node_id_unique` ON `user_nodes` (`user_id`,`node_id`);
|
||||
321
drizzle/sqlite/meta/0001_snapshot.json
Normal file
321
drizzle/sqlite/meta/0001_snapshot.json
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "d34e8a0e-f74d-4dc7-9408-1a37d9ee5df6",
|
||||
"prevId": "296005fb-1644-4912-8e82-1ebb647066e2",
|
||||
"tables": {
|
||||
"nodes": {
|
||||
"name": "nodes",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"team_id": {
|
||||
"name": "team_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"network": {
|
||||
"name": "network",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"socket": {
|
||||
"name": "socket",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"encrypted_macaroon": {
|
||||
"name": "encrypted_macaroon",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"encrypted_cert": {
|
||||
"name": "encrypted_cert",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"nodes_team_id_teams_id_fk": {
|
||||
"name": "nodes_team_id_teams_id_fk",
|
||||
"tableFrom": "nodes",
|
||||
"tableTo": "teams",
|
||||
"columnsFrom": [
|
||||
"team_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"teams": {
|
||||
"name": "teams",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user_nodes": {
|
||||
"name": "user_nodes",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"node_id": {
|
||||
"name": "node_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_nodes_user_id_node_id_unique": {
|
||||
"name": "user_nodes_user_id_node_id_unique",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"node_id"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"user_nodes_user_id_users_id_fk": {
|
||||
"name": "user_nodes_user_id_users_id_fk",
|
||||
"tableFrom": "user_nodes",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"user_nodes_node_id_nodes_id_fk": {
|
||||
"name": "user_nodes_node_id_nodes_id_fk",
|
||||
"tableFrom": "user_nodes",
|
||||
"tableTo": "nodes",
|
||||
"columnsFrom": [
|
||||
"node_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'member'"
|
||||
},
|
||||
"team_id": {
|
||||
"name": "team_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"users_team_id_teams_id_fk": {
|
||||
"name": "users_team_id_teams_id_fk",
|
||||
"tableFrom": "users",
|
||||
"tableTo": "teams",
|
||||
"columnsFrom": [
|
||||
"team_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,13 @@
|
|||
"when": 1775074036636,
|
||||
"tag": "0000_premium_pixie",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1775160646270,
|
||||
"tag": "0001_fixed_nightshade",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -81,7 +81,10 @@ type PostgresConfig = {
|
|||
url?: string;
|
||||
};
|
||||
|
||||
type DatabaseConfig = SqliteConfig | PostgresConfig | undefined;
|
||||
type DatabaseConfig =
|
||||
| (SqliteConfig & { encryptionKey?: string })
|
||||
| (PostgresConfig & { encryptionKey?: string })
|
||||
| undefined;
|
||||
|
||||
type ConfigType = {
|
||||
basePath: string;
|
||||
|
|
@ -244,10 +247,12 @@ export default (): ConfigType => {
|
|||
? {
|
||||
type: 'postgres' as const,
|
||||
url: process.env.DB_POSTGRES_URL,
|
||||
encryptionKey: process.env.DB_ENCRYPTION_KEY,
|
||||
}
|
||||
: {
|
||||
type: 'sqlite' as const,
|
||||
path: process.env.DB_SQLITE_PATH,
|
||||
encryptionKey: process.env.DB_ENCRYPTION_KEY,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
export { teams } from './teams';
|
||||
export { users } from './users';
|
||||
export { nodes } from './nodes';
|
||||
export { userNodes } from './user-nodes';
|
||||
|
|
|
|||
24
src/server/modules/database/schema/pg/nodes.ts
Normal file
24
src/server/modules/database/schema/pg/nodes.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { teams } from './teams';
|
||||
|
||||
export const nodes = pgTable('nodes', {
|
||||
id: uuid('id')
|
||||
.primaryKey()
|
||||
.default(sql`gen_random_uuid()`),
|
||||
team_id: uuid('team_id')
|
||||
.notNull()
|
||||
.references(() => teams.id),
|
||||
name: text('name').notNull(),
|
||||
type: text('type').notNull(),
|
||||
network: text('network').notNull(),
|
||||
socket: text('socket').notNull(),
|
||||
encrypted_macaroon: text('encrypted_macaroon'),
|
||||
encrypted_cert: text('encrypted_cert'),
|
||||
created_at: timestamp('created_at', { precision: 6, mode: 'string' })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updated_at: timestamp('updated_at', { precision: 6, mode: 'string' })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
});
|
||||
26
src/server/modules/database/schema/pg/user-nodes.ts
Normal file
26
src/server/modules/database/schema/pg/user-nodes.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { pgTable, uuid, timestamp, unique } from 'drizzle-orm/pg-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { users } from './users';
|
||||
import { nodes } from './nodes';
|
||||
|
||||
export const userNodes = pgTable(
|
||||
'user_nodes',
|
||||
{
|
||||
id: uuid('id')
|
||||
.primaryKey()
|
||||
.default(sql`gen_random_uuid()`),
|
||||
user_id: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
node_id: uuid('node_id')
|
||||
.notNull()
|
||||
.references(() => nodes.id),
|
||||
created_at: timestamp('created_at', { precision: 6, mode: 'string' })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updated_at: timestamp('updated_at', { precision: 6, mode: 'string' })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
},
|
||||
t => [unique().on(t.user_id, t.node_id)]
|
||||
);
|
||||
|
|
@ -1,2 +1,4 @@
|
|||
export { teams } from './teams';
|
||||
export { users } from './users';
|
||||
export { nodes } from './nodes';
|
||||
export { userNodes } from './user-nodes';
|
||||
|
|
|
|||
24
src/server/modules/database/schema/sqlite/nodes.ts
Normal file
24
src/server/modules/database/schema/sqlite/nodes.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { teams } from './teams';
|
||||
|
||||
export const nodes = sqliteTable('nodes', {
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
team_id: text('team_id')
|
||||
.notNull()
|
||||
.references(() => teams.id),
|
||||
name: text('name').notNull(),
|
||||
type: text('type').notNull(),
|
||||
network: text('network').notNull(),
|
||||
socket: text('socket').notNull(),
|
||||
encrypted_macaroon: text('encrypted_macaroon'),
|
||||
encrypted_cert: text('encrypted_cert'),
|
||||
created_at: text('created_at')
|
||||
.notNull()
|
||||
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
||||
updated_at: text('updated_at')
|
||||
.notNull()
|
||||
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
||||
});
|
||||
26
src/server/modules/database/schema/sqlite/user-nodes.ts
Normal file
26
src/server/modules/database/schema/sqlite/user-nodes.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { sqliteTable, text, unique } from 'drizzle-orm/sqlite-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { users } from './users';
|
||||
import { nodes } from './nodes';
|
||||
|
||||
export const userNodes = sqliteTable(
|
||||
'user_nodes',
|
||||
{
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
user_id: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
node_id: text('node_id')
|
||||
.notNull()
|
||||
.references(() => nodes.id),
|
||||
created_at: text('created_at')
|
||||
.notNull()
|
||||
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
||||
updated_at: text('updated_at')
|
||||
.notNull()
|
||||
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
||||
},
|
||||
t => [unique().on(t.user_id, t.node_id)]
|
||||
);
|
||||
63
src/server/utils/encryption/field-encryption.spec.ts
Normal file
63
src/server/utils/encryption/field-encryption.spec.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { randomBytes } from 'crypto';
|
||||
import { encryptValue, decryptValue } from './field-encryption';
|
||||
|
||||
const TEST_KEY = randomBytes(32).toString('hex');
|
||||
|
||||
describe('field-encryption', () => {
|
||||
describe('encryptValue / decryptValue', () => {
|
||||
it('should roundtrip a simple string', () => {
|
||||
const plaintext = 'hello world';
|
||||
const encrypted = encryptValue(plaintext, TEST_KEY);
|
||||
expect(encrypted).toMatch(/^ENC\[AES256_GCM,/);
|
||||
expect(decryptValue(encrypted, TEST_KEY)).toBe(plaintext);
|
||||
});
|
||||
|
||||
it('should roundtrip an empty string', () => {
|
||||
const encrypted = encryptValue('', TEST_KEY);
|
||||
expect(decryptValue(encrypted, TEST_KEY)).toBe('');
|
||||
});
|
||||
|
||||
it('should roundtrip a long hex string (macaroon-like)', () => {
|
||||
const macaroon = randomBytes(256).toString('hex');
|
||||
const encrypted = encryptValue(macaroon, TEST_KEY);
|
||||
expect(decryptValue(encrypted, TEST_KEY)).toBe(macaroon);
|
||||
});
|
||||
|
||||
it('should produce different ciphertexts for the same plaintext (unique IV)', () => {
|
||||
const plaintext = 'same input';
|
||||
const a = encryptValue(plaintext, TEST_KEY);
|
||||
const b = encryptValue(plaintext, TEST_KEY);
|
||||
expect(a).not.toBe(b);
|
||||
expect(decryptValue(a, TEST_KEY)).toBe(plaintext);
|
||||
expect(decryptValue(b, TEST_KEY)).toBe(plaintext);
|
||||
});
|
||||
|
||||
it('should reject an invalid key length', () => {
|
||||
expect(() => encryptValue('test', 'short')).toThrow(
|
||||
'Encryption key must be 32 bytes'
|
||||
);
|
||||
expect(() =>
|
||||
decryptValue('ENC[AES256_GCM,iv:a,tag:b,data:c]', 'short')
|
||||
).toThrow('Encryption key must be 32 bytes');
|
||||
});
|
||||
|
||||
it('should reject malformed encrypted strings', () => {
|
||||
expect(() => decryptValue('not-encrypted', TEST_KEY)).toThrow(
|
||||
'Invalid encrypted value format'
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail on tampered ciphertext (GCM auth tag check)', () => {
|
||||
const encrypted = encryptValue('secret', TEST_KEY);
|
||||
// Tamper with the data portion
|
||||
const tampered = encrypted.replace(/data:[A-Za-z0-9+/=]+/, 'data:AAAA');
|
||||
expect(() => decryptValue(tampered, TEST_KEY)).toThrow();
|
||||
});
|
||||
|
||||
it('should fail with a wrong key', () => {
|
||||
const encrypted = encryptValue('secret', TEST_KEY);
|
||||
const wrongKey = randomBytes(32).toString('hex');
|
||||
expect(() => decryptValue(encrypted, wrongKey)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
71
src/server/utils/encryption/field-encryption.ts
Normal file
71
src/server/utils/encryption/field-encryption.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
|
||||
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const IV_LENGTH = 12;
|
||||
const PREFIX = 'ENC[AES256_GCM,';
|
||||
|
||||
/**
|
||||
* Encrypts a single plaintext string using AES-256-GCM.
|
||||
* Returns a SOPS-style string: ENC[AES256_GCM,iv:<b64>,tag:<b64>,data:<b64>]
|
||||
*/
|
||||
export function encryptValue(plaintext: string, keyHex: string): string {
|
||||
const key = Buffer.from(keyHex, 'hex');
|
||||
if (key.length !== 32) {
|
||||
throw new Error('Encryption key must be 32 bytes (64 hex characters)');
|
||||
}
|
||||
|
||||
const iv = randomBytes(IV_LENGTH);
|
||||
const cipher = createCipheriv(ALGORITHM, key, iv);
|
||||
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(plaintext, 'utf8'),
|
||||
cipher.final(),
|
||||
]);
|
||||
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return `${PREFIX}iv:${iv.toString('base64')},tag:${tag.toString('base64')},data:${encrypted.toString('base64')}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a SOPS-style ENC[AES256_GCM,...] string back to plaintext.
|
||||
*/
|
||||
export function decryptValue(encrypted: string, keyHex: string): string {
|
||||
const key = Buffer.from(keyHex, 'hex');
|
||||
if (key.length !== 32) {
|
||||
throw new Error('Encryption key must be 32 bytes (64 hex characters)');
|
||||
}
|
||||
|
||||
if (!encrypted.startsWith(PREFIX) || !encrypted.endsWith(']')) {
|
||||
throw new Error('Invalid encrypted value format');
|
||||
}
|
||||
|
||||
const inner = encrypted.slice(PREFIX.length, -1);
|
||||
const parts: Record<string, string> = {};
|
||||
|
||||
for (const segment of inner.split(',')) {
|
||||
const idx = segment.indexOf(':');
|
||||
if (idx === -1) throw new Error('Invalid encrypted value format');
|
||||
parts[segment.slice(0, idx)] = segment.slice(idx + 1);
|
||||
}
|
||||
|
||||
if (
|
||||
parts.iv === undefined ||
|
||||
parts.tag === undefined ||
|
||||
parts.data === undefined
|
||||
) {
|
||||
throw new Error('Invalid encrypted value format: missing iv, tag, or data');
|
||||
}
|
||||
|
||||
const iv = Buffer.from(parts.iv, 'base64');
|
||||
const tag = Buffer.from(parts.tag, 'base64');
|
||||
const ciphertext = Buffer.from(parts.data, 'base64');
|
||||
|
||||
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
return Buffer.concat([
|
||||
decipher.update(ciphertext),
|
||||
decipher.final(),
|
||||
]).toString('utf8');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue