diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf index b2e2e367f..822b14db3 100644 --- a/docker/nginx-https.conf +++ b/docker/nginx-https.conf @@ -161,7 +161,7 @@ http { root /app/html; index index.html index.htm; expires off; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-OJgSdz/tftu3bBUxP1DqbBkfBm/fs2VjcBuZm3txHlk='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-IaCkn7qYpeosdQvIY2TXZn11ghTwUsfkSNLXPkjBcSY='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; @@ -528,24 +528,6 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } - location ~ ^/session-sharing(/.*)?$ { - proxy_pass http://127.0.0.1:30001; - proxy_http_version 1.1; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; - } - - location ~ ^/collab(/.*)?$ { - proxy_pass http://127.0.0.1:30001; - proxy_http_version 1.1; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; - } - # File manager routes (recent/pinned/shortcuts, SFTP, transfers) moved # to the file-manager plugin, served under /plugin-api/file-manager/. diff --git a/docker/nginx.conf b/docker/nginx.conf index 30ab59a93..8ae0424cf 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -142,7 +142,7 @@ http { root /app/html; index index.html index.htm; expires off; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-OJgSdz/tftu3bBUxP1DqbBkfBm/fs2VjcBuZm3txHlk='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-IaCkn7qYpeosdQvIY2TXZn11ghTwUsfkSNLXPkjBcSY='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; @@ -506,24 +506,6 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } - location ~ ^/session-sharing(/.*)?$ { - proxy_pass http://127.0.0.1:30001; - proxy_http_version 1.1; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; - } - - location ~ ^/collab(/.*)?$ { - proxy_pass http://127.0.0.1:30001; - proxy_http_version 1.1; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; - } - # File manager routes (recent/pinned/shortcuts, SFTP, transfers) moved # to the file-manager plugin, served under /plugin-api/file-manager/. diff --git a/drizzle/mysql/0040_misty_skin.sql b/drizzle/mysql/0040_misty_skin.sql new file mode 100644 index 000000000..1a7e02098 --- /dev/null +++ b/drizzle/mysql/0040_misty_skin.sql @@ -0,0 +1,6 @@ +-- session_shares, session_share_participants, collab_rooms and +-- collab_room_members moved to the session-sharing plugin, whose first +-- migration renames them. Dropping them here would lose every share and +-- room, so this only stops drizzle tracking them. SELECT 1 because MySQL +-- rejects an empty query. +SELECT 1; \ No newline at end of file diff --git a/drizzle/mysql/meta/0040_snapshot.json b/drizzle/mysql/meta/0040_snapshot.json new file mode 100644 index 000000000..0f7fdd0bc --- /dev/null +++ b/drizzle/mysql/meta/0040_snapshot.json @@ -0,0 +1,7871 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "e0d8f4e5-3151-4479-bd50-7bc5b2fdc2ce", + "prevId": "64ca1291-85aa-4417-a561-58c4fdbdf99e", + "tables": { + "ai_conversations": { + "name": "ai_conversations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + "user_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_conversations_id": { + "name": "ai_conversations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_messages": { + "name": "ai_messages", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('')" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + "conversation_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_messages_id": { + "name": "ai_messages_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_proposals": { + "name": "ai_proposals", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + "user_id", + "status" + ], + "isUnique": false + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + "conversation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_proposals_id": { + "name": "ai_proposals_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_providers": { + "name": "ai_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + "user_id", + "label" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_providers_id": { + "name": "ai_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_channels": { + "name": "automation_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + "automation_id", + "channel_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_channels_id": { + "name": "automation_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_run_steps": { + "name": "automation_run_steps", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "run_id": { + "name": "run_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_index": { + "name": "step_index", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truncated": { + "name": "truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + "run_id", + "step_index" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_run_steps_id": { + "name": "automation_run_steps_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_runs": { + "name": "automation_runs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + "automation_id", + "started_at" + ], + "isUnique": false + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_runs_id": { + "name": "automation_runs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_schedules": { + "name": "automation_schedules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + "automation_id" + ], + "isUnique": true + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + "next_due_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_schedules_id": { + "name": "automation_schedules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_trigger_state": { + "name": "automation_trigger_state", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state_key": { + "name": "state_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_value": { + "name": "last_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + "automation_id", + "state_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_trigger_state_id": { + "name": "automation_trigger_state_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automations": { + "name": "automations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "definition_version": { + "name": "definition_version", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('skip')" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + "user_id", + "enabled" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automations_id": { + "name": "automations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('use')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_access_id": { + "name": "credential_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "folder_access_id": { + "name": "folder_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_web_ui": { + "name": "enable_web_ui", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_ai_assistant": { + "name": "enable_ai_assistant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "web_ui_config": { + "name": "web_ui_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('aggregate-telemetry')" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_install_counts_id": { + "name": "plugin_install_counts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_migrations_id": { + "name": "plugin_migrations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('admin')" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_permission_grants_id": { + "name": "plugin_permission_grants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_registries_id": { + "name": "plugin_registries_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_settings_id": { + "name": "plugin_settings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_storage_id": { + "name": "plugin_storage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('available')" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('disabled')" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugins_id": { + "name": "plugins_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_applied_defaults_id": { + "name": "rbac_applied_defaults_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_known_permissions_id": { + "name": "rbac_known_permissions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "secret_sources": { + "name": "secret_sources", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('onepassword-connect')" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_secret_sources_user": { + "name": "idx_secret_sources_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "secret_sources_user_id_users_id_fk": { + "name": "secret_sources_user_id_users_id_fk", + "tableFrom": "secret_sources", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "secret_sources_id": { + "name": "secret_sources_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('password')" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_credential_secrets_id": { + "name": "shared_credential_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ui_preferences_user_id": { + "name": "ui_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_external_identities_id": { + "name": "user_external_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_defaults": { + "name": "rdp_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_second_factors_id": { + "name": "user_second_factors_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/_journal.json b/drizzle/mysql/meta/_journal.json index 15bdc18e5..61e74bbe0 100644 --- a/drizzle/mysql/meta/_journal.json +++ b/drizzle/mysql/meta/_journal.json @@ -281,6 +281,13 @@ "when": 1790220944905, "tag": "0039_condemned_shiva", "breakpoints": true + }, + { + "idx": 40, + "version": "5", + "when": 1790224430728, + "tag": "0040_misty_skin", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/postgres/0041_watery_zeigeist.sql b/drizzle/postgres/0041_watery_zeigeist.sql new file mode 100644 index 000000000..1a7e02098 --- /dev/null +++ b/drizzle/postgres/0041_watery_zeigeist.sql @@ -0,0 +1,6 @@ +-- session_shares, session_share_participants, collab_rooms and +-- collab_room_members moved to the session-sharing plugin, whose first +-- migration renames them. Dropping them here would lose every share and +-- room, so this only stops drizzle tracking them. SELECT 1 because MySQL +-- rejects an empty query. +SELECT 1; \ No newline at end of file diff --git a/drizzle/postgres/meta/0041_snapshot.json b/drizzle/postgres/meta/0041_snapshot.json new file mode 100644 index 000000000..5cb2762e7 --- /dev/null +++ b/drizzle/postgres/meta/0041_snapshot.json @@ -0,0 +1,7677 @@ +{ + "id": "1d75d039-d690-443a-87cb-96e79a672303", + "prevId": "bc0056a1-84fe-4307-81b3-2b3d8ca92080", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ai_conversations": { + "name": "ai_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_messages": { + "name": "ai_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_proposals": { + "name": "ai_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_providers": { + "name": "ai_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_channels": { + "name": "automation_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run_steps": { + "name": "automation_run_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "step_index": { + "name": "step_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "truncated": { + "name": "truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_runs": { + "name": "automation_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_schedules": { + "name": "automation_schedules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + { + "expression": "next_due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_trigger_state": { + "name": "automation_trigger_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "state_key": { + "name": "state_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_value": { + "name": "last_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automations": { + "name": "automations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "definition_version": { + "name": "definition_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'skip'" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_access": { + "name": "credential_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folder_access": { + "name": "folder_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "folder", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_web_ui": { + "name": "enable_web_ui", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_ai_assistant": { + "name": "enable_ai_assistant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_ui_config": { + "name": "web_ui_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + { + "expression": "parent_host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_install_counts": { + "name": "plugin_install_counts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_migrations": { + "name": "plugin_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_permission_grants": { + "name": "plugin_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "capability", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_registries": { + "name": "plugin_registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_settings": { + "name": "plugin_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_storage": { + "name": "plugin_storage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + { + "expression": "role_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_known_permissions": { + "name": "rbac_known_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret_sources": { + "name": "secret_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'onepassword-connect'" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_secret_sources_user": { + "name": "idx_secret_sources_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secret_sources_user_id_users_id_fk": { + "name": "secret_sources_user_id_users_id_fk", + "tableFrom": "secret_sources", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_credential_secrets": { + "name": "shared_credential_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + { + "expression": "credential_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ui_preferences": { + "name": "ui_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_external_identities": { + "name": "user_external_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_defaults": { + "name": "rdp_defaults", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_second_factors": { + "name": "user_second_factors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "factor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/_journal.json b/drizzle/postgres/meta/_journal.json index a5c9ce17a..6ca58b73b 100644 --- a/drizzle/postgres/meta/_journal.json +++ b/drizzle/postgres/meta/_journal.json @@ -288,6 +288,13 @@ "when": 1790220944274, "tag": "0040_narrow_hellion", "breakpoints": true + }, + { + "idx": 41, + "version": "7", + "when": 1790224430003, + "tag": "0041_watery_zeigeist", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/sqlite/0037_burly_enchantress.sql b/drizzle/sqlite/0037_burly_enchantress.sql new file mode 100644 index 000000000..1a7e02098 --- /dev/null +++ b/drizzle/sqlite/0037_burly_enchantress.sql @@ -0,0 +1,6 @@ +-- session_shares, session_share_participants, collab_rooms and +-- collab_room_members moved to the session-sharing plugin, whose first +-- migration renames them. Dropping them here would lose every share and +-- room, so this only stops drizzle tracking them. SELECT 1 because MySQL +-- rejects an empty query. +SELECT 1; \ No newline at end of file diff --git a/drizzle/sqlite/meta/0037_snapshot.json b/drizzle/sqlite/meta/0037_snapshot.json new file mode 100644 index 000000000..90d780420 --- /dev/null +++ b/drizzle/sqlite/meta/0037_snapshot.json @@ -0,0 +1,7423 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "da414c7f-c198-46ea-bb9b-48373ee8cbb1", + "prevId": "e52992d9-f742-43d4-9e85-38128158c7a1", + "tables": { + "ai_conversations": { + "name": "ai_conversations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + "user_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_messages": { + "name": "ai_messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + "conversation_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_proposals": { + "name": "ai_proposals", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + "user_id", + "status" + ], + "isUnique": false + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + "conversation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_providers": { + "name": "ai_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + "user_id", + "label" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_channels": { + "name": "automation_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + "automation_id", + "channel_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_run_steps": { + "name": "automation_run_steps", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "run_id": { + "name": "run_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_index": { + "name": "step_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truncated": { + "name": "truncated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + "run_id", + "step_index" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_runs": { + "name": "automation_runs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dry_run": { + "name": "dry_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + "automation_id", + "started_at" + ], + "isUnique": false + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_schedules": { + "name": "automation_schedules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + "automation_id" + ], + "isUnique": true + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + "next_due_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_trigger_state": { + "name": "automation_trigger_state", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_value": { + "name": "last_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + "automation_id", + "state_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automations": { + "name": "automations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "definition_version": { + "name": "definition_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'skip'" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + "user_id", + "enabled" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_web_ui": { + "name": "enable_web_ui", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_ai_assistant": { + "name": "enable_ai_assistant", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "web_ui_config": { + "name": "web_ui_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_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 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "secret_sources": { + "name": "secret_sources", + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'onepassword-connect'" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_secret_sources_user": { + "name": "idx_secret_sources_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "secret_sources_user_id_users_id_fk": { + "name": "secret_sources_user_id_users_id_fk", + "tableFrom": "secret_sources", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "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 + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "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 + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "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 + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_defaults": { + "name": "rdp_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/_journal.json b/drizzle/sqlite/meta/_journal.json index 8a017533c..aaee8cd66 100644 --- a/drizzle/sqlite/meta/_journal.json +++ b/drizzle/sqlite/meta/_journal.json @@ -260,6 +260,13 @@ "when": 1790220943638, "tag": "0036_classy_nighthawk", "breakpoints": true + }, + { + "idx": 37, + "version": "6", + "when": 1790224429343, + "tag": "0037_burly_enchantress", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 074b456bb..0b435d23e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7016,6 +7016,10 @@ "resolved": "plugins/serial", "link": true }, + "node_modules/@termix-plugin/session-sharing": { + "resolved": "plugins/session-sharing", + "link": true + }, "node_modules/@termix-plugin/snippets": { "resolved": "plugins/snippets", "link": true @@ -7028,6 +7032,10 @@ "resolved": "plugins/tailscale", "link": true }, + "node_modules/@termix-plugin/tmux-monitor": { + "resolved": "plugins/tmux-monitor", + "link": true + }, "node_modules/@termix-plugin/tunnels": { "resolved": "plugins/tunnels", "link": true @@ -8132,7 +8140,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", - "dev": true, "license": "MIT" }, "node_modules/@xterm/addon-search": { @@ -8160,7 +8167,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", - "dev": true, "license": "MIT", "workspaces": [ "addons/*" @@ -15924,7 +15930,6 @@ "version": "1.0.12", "resolved": "https://registry.npmjs.org/react-xtermjs/-/react-xtermjs-1.0.12.tgz", "integrity": "sha512-8uXBzVF4vP6r2NABxt/9heH1Y5Vx2v2hFrAk1kre+HyY3CjM6Prbc8ViqBRia6EBzI57IFBWOicYV1/k05JBtw==", - "dev": true, "license": "ISC", "peerDependencies": { "@xterm/xterm": "^5.5.0" @@ -18609,6 +18614,19 @@ "@termix/plugin-sdk": "*" } }, + "plugins/session-sharing": { + "name": "@termix-plugin/session-sharing", + "version": "1.0.0", + "dependencies": { + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "react-xtermjs": "^1.0.10", + "redis": "^6.2.1" + }, + "devDependencies": { + "@termix/plugin-sdk": "*" + } + }, "plugins/snippets": { "name": "@termix-plugin/snippets", "version": "1.0.0", @@ -18630,6 +18648,13 @@ "@termix/plugin-sdk": "*" } }, + "plugins/tmux-monitor": { + "name": "@termix-plugin/tmux-monitor", + "version": "1.0.0", + "devDependencies": { + "@termix/plugin-sdk": "*" + } + }, "plugins/tunnels": { "name": "@termix-plugin/tunnels", "version": "1.0.0", diff --git a/packages/plugin-sdk/ARCHITECTURE.md b/packages/plugin-sdk/ARCHITECTURE.md index 33391833e..7f2434db5 100644 --- a/packages/plugin-sdk/ARCHITECTURE.md +++ b/packages/plugin-sdk/ARCHITECTURE.md @@ -367,6 +367,14 @@ Anonymous shared-session and collab pages have no session. They load only plugins whose manifest sets `contributes.guest: true`, from the public `GET /plugins/public`, and `app.guest` is true there. +**B12** made those pages generic. A guest plugin lists the `?view=` names it +serves anonymously in `contributes.guestViews` (which needs `guest: true`), +and `/plugins/public` returns them. `src/main.tsx` asks for that list when a +URL carries `?view=`: a guest view starts the guest runtime and renders the +tab type whose `standalone`/`standaloneViews` claims it, anything else goes +through the signed-in full-screen gate as before. The shell no longer knows +`?view=shared` or `?view=collab-guest`; session-sharing declares both. + The login screen runs a smaller pass before anyone signs in: it activates only enabled plugins that contribute `loginMethods` or `secondFactors`, listed by the public `GET /plugins/public-manifest`. They stay active after login, and @@ -426,7 +434,7 @@ boundary and Suspense. | `registerLoginMethod`, `registerSecondFactorUI` | Login screen UI for a method and a second factor challenge (plus an optional `enrollment` section) | | `api`, `wsUrl(path)` | axios on `/plugin-api//` and the plugin's WebSocket URL | | `t`, `hasPermission` | The plugin's strings and a permission check, for code outside a component (a toast from `activate`) | -| `tabs.open`, `getLayout`, `applyLayout`, `onChange` | Tab control, used by workspaces | +| `tabs.open`, `getLayout`, `applyLayout`, `onChange` | Tab control, used by workspaces; `tabs.openRailView` (**B12**) opens a rail view | | `guest`, `info`, `onDispose` | Guest mode flag, plugin info, extra cleanup | Hooks: `useTranslation` (the plugin's namespace), `usePermission` (a short @@ -445,7 +453,14 @@ Slots core owns: `terminal.toolbar`, `terminal.toolbarStatus`, renamed `terminal.dock` to `terminal.sidePanel` and added the status slot, where host-metrics puts its CPU, memory and disk bars), `onboarding.steps`, `onboarding.features`, `onboarding.workflow`, `hosts.importMenu`, `hosts.panel`, `proxmox.hostEditor` -and `session.remoteDisplay`. `host-metrics.managers` (component slot, +and `shell.overlay`. **B12** added `shell.overlay`, a component slot the +shell renders once at its root for always-mounted plugin UI: session-sharing +keeps its share dialog and its room-invite watcher there. +`session.remoteDisplay` moved to session-sharing, which declares it and draws +whatever remote desktop contributes to it in rooms and share links. +`remote-desktop.toolbar` (button slot, **B12**) is remote desktop's own, +invoked with `{ hostId, sessionId, protocol, tabInstanceId }`. +`host-metrics.managers` (component slot, **B3**) is host-metrics's own: tailscale contributes its manager card there instead of host-metrics knowing tailscale exists, and host-metrics works with or without tailscale enabled. Cross-plugin frontend calls go through actions: @@ -474,7 +489,7 @@ tools act on the last one focused; `PanelProps.targetTab` hands a panel that tab), `ownBackground` (the shell leaves the frame transparent) and `multiInstance` (each open is a new tab). `TabHandle` documents what a session tab puts on `handleRef`: focus, reconnect, disconnect and, for sharing, -`getShareTarget()`, which the shell's own share dialog uses. These replaced +`getShareTarget()` for sharing. These replaced every check of the `terminal` and `local-terminal` tab types in the shell. **B2** added a live-terminal-session surface, for a plugin that needs to push @@ -598,13 +613,35 @@ who has no user, cannot make one. Guest link resolution is therefore published on `ctx.registry` as `sessions.sharing.guests` instead: the token is the authority there. -**Core consuming a plugin service.** Session sharing, collab and the open -tabs route are still core and reach live terminal sessions through -`getServiceImplementation(service, range)` in `service-registry.ts` +**Core consuming a plugin service.** The open tabs route is still core and +lists the caller's own terminal sessions through +`getServiceImplementation(service, range, name?)` in `service-registry.ts` (`hosts/live-terminal-sessions.ts` wraps it). Core already authorized the request, so this skips the per-call check and returns the same object plugins get, or undefined while no compatible provider runs. +**Named providers (B12).** A service several plugins provide side by side is +keyed by a provider name. The manifest lists the names in +`provides[].names`, `ctx.services.provide(service, impl, { name })` refuses +any other, `ctx.services.get(service, { provider })` reaches one, and +`ctx.services.providers(service)` lists the running ones. A requirement is +satisfied by any compatible provider. `sessions.live` is the first: keyed by +session type, ssh-terminal provides `ssh` and remote desktop provides `rdp`, +`vnc` and `telnet` (B14), with `createViewerToken(sessionId, readOnly)` for +a viewer. A consumer with no actor, such as a public guest route, calls it +through `ctx.asUser()`. + +**Session sharing (B12)** is the session-sharing plugin. It provides +`sessions.sharing` (member joins, room events) and `sessions.sharing.guests`, +adopts `session_shares`, `session_share_participants`, `collab_rooms` and +`collab_room_members`, and reaches sessions only through `sessions.live`. Its +two public routes, `/resolve/:linkToken` and `/guest/:token`, check the token +themselves and are rate limited per IP. The share button is a contribution to +`terminal.toolbar` and `remote-desktop.toolbar`; the tab bar's share entry and +the `shareable`, `canShare`, `openShareModal` and `getShareTarget` members of +`TabOptions`/`TabHandle` are gone. Core's active connections list gets the +sessions shared with a user from the `sessions.sharedWithMe` action. + ### 10. Lifecycle `activate(ctx)` creates all state. Everything registered through `ctx` or `app` @@ -638,7 +675,7 @@ not care about the gates, `createTestDb(pluginDir)` for a real database, and `renderWithApp(plugin, options)` for frontend tests. `createTestDb(pluginDir, { before })` opens an in-memory SQLite with stub -`users`, `ssh_data`, `roles` and `user_roles` tables and foreign keys on, runs +`users` (with `is_admin`, **B12**), `ssh_data`, `roles` and `user_roles` tables and foreign keys on, runs `before` (seed a legacy table there to test an adoption), then applies the plugin's own `migrations/sqlite` with the runner's splitter. `ctx.db.refs()` against this database returns the same four, built by `buildRefTable`, which @@ -653,7 +690,10 @@ against the real schema's property names works unchanged in a test. Pass its and `hosts` (`PluginHostSummary[]`, what `ctx.hosts.list/get/checkAccess` serve - **B8** added this option, since it existed on `createFakeContext` already but was not forwarded), and returns `setActor(userId)` for a test -middleware and `services` for what the plugin provided. **B8** also gave +middleware and `services` for what the plugin provided. **B12** added a +`services` option seeding other plugins' services for a consumer's test, +keyed `` or `#`, and the doubles now answer a +missing service with an empty handle, like the runtime, instead of throwing. **B8** also gave `ctx.registry` on both doubles a real in-memory backing (it was a no-op stub before), so `provide`/`consume`/`revoke` round-trip the way the real registry does. `plugins/workspaces/tests/backend/helpers.ts` is the worked example. @@ -1068,7 +1108,10 @@ Cross-field rules the JSON schema cannot express, checked by `parseManifest`: `contributes.panels` and `contributes.dashboardCards` (each `{ id, titleKey, icon? }`) declare the views a plugin owns besides its tabs; the app object refuses to register a view the manifest does not declare. `contributes.guest` -opts a plugin into anonymous guest pages. Action contributions and slots +opts a plugin into anonymous guest pages, and `contributes.guestViews` names +the `?view=` pages it serves there. `provides[].names` lists the named +providers a plugin registers for a service keyed by provider (see Cross-plugin +use). Action contributions and slots accept the kinds `button` and `component`. `contributes.auth` lists the ids a plugin registers through `ctx.auth`. A @@ -1148,7 +1191,7 @@ Built per plugin in `src/backend/plugins/ctx.ts` and passed to `activate`. | `ctx.events.emit` / `.on` | `events:core` for core topics | **A1** | | `ctx.kv.get/set/delete/list` | `kv:own` | **A1** | | `ctx.registry.*` | none | **A1** | -| `ctx.services.provide` / `.get` | per-service RBAC | **A1** | +| `ctx.services.provide` / `.get` / `.providers` | per-service RBAC | **A1**, named **B12** | | `ctx.secrets.offer` / `.withdraw` / `.getShared` | per-secret RBAC | **A1** | | `ctx.disposables.add` | none | **A1** | | `ctx.asUser(userId, fn)` | none, always audited | **A1** | diff --git a/packages/plugin-sdk/FINISH-LIST.md b/packages/plugin-sdk/FINISH-LIST.md index 6998c4aff..2400b9b5f 100644 --- a/packages/plugin-sdk/FINISH-LIST.md +++ b/packages/plugin-sdk/FINISH-LIST.md @@ -144,21 +144,6 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite to ask Electron's main process for something, extend `BACKEND_REQUEST_HANDLERS` there rather than building a parallel channel. No owner needed unless a second caller appears. -- **B9 (ssh-terminal):** session sharing has no provider yet. The terminal - consumes `sessions.sharing` v1 (member joins through `authorizeJoin` and - `recordJoin`, collab room events through `subscribeRoom`/`unsubscribeRoom`) - and reads guest link resolution from `ctx.registry` as - `sessions.sharing.guests` (`resolve`, `recordJoin`), all typed in - `plugins/ssh-terminal/src/backend/services.ts`. Until something provides - them, share-link and room guests are refused and in-app joins fail. Core's - session-sharing, collab and open-tabs code reach the terminal through - `sessions.live` already. Owner: B12, which provides both from the - session-sharing plugin (the lookups are the old code in - `hosts/session-sharing` and `hosts/collab`: share repo, room repo, guest - rate limit, `canJoinRoomStageShare`, `session_sharing_globally_enabled`, - host `allowSessionSharing`, `getAuditUsername` for labels, `collabRoomHub`). - B12 also moves `eventsWsPath` in `collab/routes.ts` and the share dialog the - shell opens from a tab's `getShareTarget()`. - **B9 (ssh-terminal), for D2:** the terminal socket is a public route with `optionalAuth`, because share-link and room guests authenticate inside the handler with their token. Guest auth itself is unchanged; review it with the @@ -270,6 +255,34 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite section. Owner: whoever designs the 3.0.0 install flow (prebuild-per-platform packaging, or a documented "avoid native code" rule for community plugins). +- **B12 (session-sharing):** the remote desktop side of sharing has no + provider yet. Session sharing reaches rdp, vnc and telnet sessions through + `sessions.live` named providers `rdp`, `vnc` and `telnet`, each with + `getSession(sessionId)` (id, userId as the session's owner, hostId, + hostName, isConnected, createdAt, tabInstanceId) and + `createViewerToken(sessionId, readOnly)`, typed in + `plugins/session-sharing/src/backend/live.ts`. Until remote-desktop provides + them, sharing or presenting an RDP/VNC/Telnet session is refused and those + links resolve 404. B14 also drops `createJoinToken`/`getSessionInfo` from + its `remote-desktop.sessions` registry entry, which core no longer reads. + Owner: B14. +- **B12 (session-sharing):** `allow_session_sharing` is still a live `ssh_data` + column. The copy into the plugin's host setting shipped + (`session-sharing-settings-migration.ts`) and nothing in core reads or + writes it any more; drop it in lockstep (`schema.ts`, `db/index.ts`, a + drizzle migration per dialect, `schema:generate`). Owner: D0. +- **B12 (session-sharing):** the legacy `session_sharing_globally_enabled` + settings row is left in place for a release; nothing reads it. Delete it in + 3.0.0. Owner: D0 to decide. +- **B12 (session-sharing):** room audit lines go through `ctx.audit.record`, + which records the actor but not the request's IP address and user agent the + old `logAudit` calls carried. Adding those to `ctx.audit.record` would + restore them. Owner: D1. +- **B12 (session-sharing):** the share button lives in the terminal and remote + desktop toolbars now; the tab bar's share button is gone, so a host with the + terminal toolbar switched off has no share button. A generic tab-menu slot + would bring it back if that matters. Owner: D1, if wanted. + ## Manual checks after 2.9.0 - SSH terminal: password auth, key auth (and an encrypted key's passphrase @@ -278,3 +291,7 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite page reload (session reattach), and the local terminal in the desktop app. - Serial: the Electron backend path against a real device path/COM port, and the Web Serial browser picker in Chrome or Edge. +- Session sharing: a link share (read-only and read-write) opened in a private + window, a user share joined from Active Connections, revoke and "end for + everyone", and a collab room with two members, control hand-off, a guest + link, rotate and end. diff --git a/packages/plugin-sdk/schema/manifest.schema.json b/packages/plugin-sdk/schema/manifest.schema.json index 47fb2ade7..5df7e9438 100644 --- a/packages/plugin-sdk/schema/manifest.schema.json +++ b/packages/plugin-sdk/schema/manifest.schema.json @@ -162,6 +162,12 @@ "permission": { "type": "string", "minLength": 1 + }, + "names": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" } } } } @@ -265,6 +271,12 @@ } }, "guest": { "type": "boolean" }, + "guestViews": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" } + }, "auth": { "type": "object", "additionalProperties": false, diff --git a/packages/plugin-sdk/src/backend.ts b/packages/plugin-sdk/src/backend.ts index a89eb7941..2f8d2f5fd 100644 --- a/packages/plugin-sdk/src/backend.ts +++ b/packages/plugin-sdk/src/backend.ts @@ -179,8 +179,23 @@ export interface PluginRegistry { } export interface PluginServices { - provide: (service: string, implementation: T) => void; - get: (service: string, options?: { userId?: string }) => T; + /** + * `name` registers one of several providers of the same service, for a + * service keyed by something the caller picks (sessions.live by session + * type). It must be listed in the manifest's `provides[].names`. + */ + provide: ( + service: string, + implementation: T, + options?: { name?: string }, + ) => void; + /** `provider` picks a named provider; omitted, the unnamed one. */ + get: ( + service: string, + options?: { userId?: string; provider?: string }, + ) => T; + /** Names of the providers running now ("" for an unnamed one). */ + providers: (service: string) => string[]; } export interface PluginSecrets { diff --git a/packages/plugin-sdk/src/frontend.ts b/packages/plugin-sdk/src/frontend.ts index e2df21b17..d01692246 100644 --- a/packages/plugin-sdk/src/frontend.ts +++ b/packages/plugin-sdk/src/frontend.ts @@ -96,7 +96,7 @@ export interface ShellApi { export interface TabsApi extends Pick< ShellApi, - "openTab" | "openSingletonTab" | "closeTab" + "openTab" | "openSingletonTab" | "closeTab" | "openRailView" > { /** The current tabs and split layout, or null before the shell mounts. */ getLayout: () => ShellLayout | null; @@ -187,16 +187,6 @@ export interface TabHandle { paste?: (text: string) => void; refresh?: () => void; notifyResize?: () => void; - /** A tab that shows its own share dialog. */ - canShare?: () => boolean; - openShareModal?: () => void; - /** A tab whose session the shell's share dialog can share. */ - getShareTarget?: () => { - hostId: number; - sessionId: string; - protocol: "ssh"; - tabInstanceId?: string; - } | null; [key: string]: unknown; } @@ -232,12 +222,6 @@ export interface TabOptions { singleton?: boolean; /** A live session: asks before closing and can be refreshed. */ session?: boolean; - /** - * Whether a session tab offers "Share session". Default true. A plugin - * whose ref does not implement session sharing (a local device, not a - * remote connection another user could join) sets this false. - */ - shareable?: boolean; /** Restorable without a host. */ hostless?: boolean; /** Decides whether a saved tab for this host may be restored. */ diff --git a/packages/plugin-sdk/src/manifest.ts b/packages/plugin-sdk/src/manifest.ts index de7e18332..4e0ab4522 100644 --- a/packages/plugin-sdk/src/manifest.ts +++ b/packages/plugin-sdk/src/manifest.ts @@ -145,6 +145,12 @@ export interface PluginServiceProvide { version: string; /** The role permission a calling user needs. */ permission: string; + /** + * Named providers this plugin registers, for a service several plugins + * provide side by side (sessions.live, keyed by session type). Omitted, the + * plugin provides it once, unnamed. + */ + names?: string[]; } export interface PluginServiceRequire { @@ -256,6 +262,11 @@ export interface PluginContributions { * collab links), where it is activated with app.guest set. */ guest?: boolean; + /** + * `?view=` names an anonymous guest page may open. Each must be served by a + * tab's `standalone` component. Needs `guest: true`. + */ + guestViews?: string[]; actions?: PluginActionContribution[]; actionSlots?: PluginActionSlot[]; permissions?: PluginPermissionContribution[]; @@ -348,6 +359,7 @@ const ALLOWED_CONTRIBUTES = new Set([ "panels", "dashboardCards", "guest", + "guestViews", "actions", "actionSlots", "permissions", @@ -598,7 +610,12 @@ function validateProvides(provides: unknown, errors: string[]): void { errors.push(`${where} must be an object`); return; } - rejectUnknown(raw, ["service", "version", "permission"], where, errors); + rejectUnknown( + raw, + ["service", "version", "permission", "names"], + where, + errors, + ); if (typeof raw.service !== "string" || !SERVICE_PATTERN.test(raw.service)) { errors.push(`${where}.service must be a dotted lowercase name`); @@ -613,9 +630,30 @@ function validateProvides(provides: unknown, errors: string[]): void { errors.push(`${where}.version must be valid semver`); } requireString(raw.permission, `${where}.permission`, errors); + if (raw.names !== undefined) { + validateNameList(raw.names, `${where}.names`, errors); + } }); } +const PROVIDER_NAME_PATTERN = /^[a-z][a-z0-9-]*$/; + +function validateNameList(value: unknown, where: string, errors: string[]) { + if (!Array.isArray(value) || value.length === 0) { + errors.push(`${where} must be a non-empty array`); + return; + } + const seen = new Set(); + for (const name of value) { + if (typeof name !== "string" || !PROVIDER_NAME_PATTERN.test(name)) { + errors.push(`${where} entries must be lowercase names`); + continue; + } + if (seen.has(name)) errors.push(`${where} duplicates "${name}"`); + seen.add(name); + } +} + function validateRequires(requires: unknown, errors: string[]): void { if (requires === undefined) return; if (!Array.isArray(requires)) { @@ -721,6 +759,12 @@ function validateContributes( ) { errors.push('Field "contributes.guest" must be a boolean'); } + if (contributes.guestViews !== undefined) { + validateNameList(contributes.guestViews, "contributes.guestViews", errors); + if (contributes.guest !== true) { + errors.push('Field "contributes.guestViews" needs "contributes.guest"'); + } + } validatePermissions(contributes.permissions, pluginId, errors); validateActions(contributes.actions, errors); validateActionSlots(contributes.actionSlots, errors); diff --git a/packages/plugin-sdk/src/table-builder.ts b/packages/plugin-sdk/src/table-builder.ts index d89272db0..d04f37e18 100644 --- a/packages/plugin-sdk/src/table-builder.ts +++ b/packages/plugin-sdk/src/table-builder.ts @@ -104,15 +104,17 @@ export function buildTable( */ export function buildRefTable( name: string, - columns: Record, + columns: Record, ): unknown { const built: Record = {}; for (const [property, type] of Object.entries(columns)) { const columnSqlName = columnName(property); built[property] = - type === "integer" - ? sqliteInteger(columnSqlName) - : sqliteText(columnSqlName); + type === "boolean" + ? sqliteInteger(columnSqlName, { mode: "boolean" }) + : type === "integer" + ? sqliteInteger(columnSqlName) + : sqliteText(columnSqlName); } return sqliteTable(name, built as never); } diff --git a/packages/plugin-sdk/src/testing.ts b/packages/plugin-sdk/src/testing.ts index 265625d50..fea1f436a 100644 --- a/packages/plugin-sdk/src/testing.ts +++ b/packages/plugin-sdk/src/testing.ts @@ -57,6 +57,11 @@ export interface FakeContextOptions { * when omitted every check passes, so tests that do not care stay simple. */ permissions?: string[]; + /** + * Services other plugins provide, for testing a consumer. Keyed by service + * name, or "#" for a named provider. + */ + services?: Record; /** Hosts ctx.hosts.list/get/checkAccess answer with. */ hosts?: PluginHostSummary[]; /** Users and roles ctx.hosts.listUsers/listRoles answer with. */ @@ -132,7 +137,7 @@ export interface FakePluginContext { }>; /** Changes the acting user, as core's request middleware would. */ setActor: (userId: string | undefined) => void; - /** Implementations provided through ctx.services.provide, by service name. */ + /** Provided or seeded services, by name or "#". */ services: Map; } @@ -210,7 +215,11 @@ export function createFakeContext( ); const hostRecordsById = new Map(); let nextHostId = Math.max(0, ...(options.hosts ?? []).map((h) => h.id)) + 1; - const services = new Map(); + const services = new Map( + Object.entries(options.services ?? {}), + ); + const serviceKey = (service: string, name?: string) => + name ? `${service}#${name}` : service; const registryProviders = new Map(); const auth: FakeAuthRegistrations = { sshAuthProviders: [], @@ -349,18 +358,23 @@ export function createFakeContext( }, services: { - provide: (service, implementation) => { - services.set(service, implementation); - }, - // Only services this same context provided. A test of a consumer - // should provide a stub first. - get: (service) => { - const implementation = services.get(service); - if (!implementation) { - throw new Error(`No service "${service}" was provided in this test`); - } - return implementation as never; + provide: (service, implementation, provideOptions) => { + services.set(serviceKey(service, provideOptions?.name), implementation); }, + // What this context provided or the test seeded through `services`. + // Like the real handle, a missing provider is an empty object, which a + // consumer's `"method" in handle` probe reads as absent. + get: (service, getOptions) => + (services.get(serviceKey(service, getOptions?.provider)) ?? + {}) as never, + providers: (service) => + [...services.keys()].flatMap((key) => + key === service + ? [""] + : key.startsWith(`${service}#`) + ? [key.slice(service.length + 1)] + : [], + ), }, secrets: { @@ -680,6 +694,8 @@ export interface MockContextOptions { permissions?: string[]; /** Hosts ctx.hosts.list/get/checkAccess serve. See FakeContextOptions. */ hosts?: PluginHostSummary[]; + /** Other plugins' services. See FakeContextOptions. */ + services?: Record; /** Hosts ctx.ssh.resolveHost serves. See FakeContextOptions. */ sshHosts?: PluginSshHost[]; } @@ -717,6 +733,7 @@ export function createMockCtx( permissions: options.permissions, hosts: options.hosts, sshHosts: options.sshHosts, + services: options.services, manifest: { capabilities: options.capabilities ?? [], ...options.manifest, @@ -1018,7 +1035,7 @@ export async function createTestDb( const sqlite = new Database(":memory:") as TestSqlite; sqlite.exec("PRAGMA foreign_keys = ON"); sqlite.exec( - "CREATE TABLE users (id TEXT PRIMARY KEY, username TEXT NOT NULL DEFAULT '')", + "CREATE TABLE users (id TEXT PRIMARY KEY, username TEXT NOT NULL DEFAULT '', is_admin INTEGER NOT NULL DEFAULT 0)", ); sqlite.exec("CREATE TABLE ssh_data (id INTEGER PRIMARY KEY AUTOINCREMENT)"); sqlite.exec( @@ -1059,7 +1076,11 @@ export async function createTestDb( if (!options.skipMigrations) await migrate(); const refs = { - users: builder.buildRefTable("users", { id: "text", username: "text" }), + users: builder.buildRefTable("users", { + id: "text", + username: "text", + isAdmin: "boolean", + }), hosts: builder.buildRefTable("ssh_data", { id: "integer" }), roles: builder.buildRefTable("roles", { id: "integer", diff --git a/plugins/ai/src/backend/gating.ts b/plugins/ai/src/backend/gating.ts index 0967519ea..b2ceb54fd 100644 --- a/plugins/ai/src/backend/gating.ts +++ b/plugins/ai/src/backend/gating.ts @@ -12,8 +12,8 @@ import { * exist for anyone, regardless of what any user has enabled. It defaults to * false so upgrading an existing install turns nothing on by surprise. * - * Mirrors the shape of isSharingEnabledForHost in the session-sharing routes, - * where the global also wins over the per-entity setting. + * Mirrors session sharing's own check, where the global also wins over the + * per-host setting. */ export const AI_GLOBAL_ENABLED_KEY = "ai_globally_enabled"; diff --git a/plugins/remote-desktop/manifest.json b/plugins/remote-desktop/manifest.json index 8ae158bd2..68fe72225 100644 --- a/plugins/remote-desktop/manifest.json +++ b/plugins/remote-desktop/manifest.json @@ -60,6 +60,12 @@ "editorTab": "telnet" } ], + "actionSlots": [ + { + "id": "remote-desktop.toolbar", + "accepts": ["button"] + } + ], "guest": true }, "locales": "locales", diff --git a/plugins/remote-desktop/src/frontend/GuacamoleApp.tsx b/plugins/remote-desktop/src/frontend/GuacamoleApp.tsx index d7c699f6a..0e19b9115 100644 --- a/plugins/remote-desktop/src/frontend/GuacamoleApp.tsx +++ b/plugins/remote-desktop/src/frontend/GuacamoleApp.tsx @@ -45,7 +45,6 @@ import { ConnectionLogProvider, useConnectionLog, } from "@/ssh/connection-log/ConnectionLogContext.tsx"; -import { ShareSessionModal } from "@/features/session-sharing/ShareSessionModal.tsx"; import type { SSHHost } from "@/types"; import { useConnectionDefaults } from "@/contexts/ConnectionDefaultsContext"; import { resolveConnectionDefaults } from "@/lib/connection-defaults"; @@ -68,8 +67,6 @@ export type GuacamoleQuickHost = GuacamoleAppInnerProps["hostConfig"] & { export interface GuacamoleAppHandle { disconnect: () => void; isConnected: () => boolean; - openShareModal: () => void; - canShare: () => boolean; /** Reconnects, from the tab bar's refresh. */ refresh: () => void; } @@ -202,7 +199,6 @@ const GuacamoleAppInner = React.forwardRef< id: string; origin: ConnectionOrigin; } | null>(null); - const [shareModalOpen, setShareModalOpen] = useState(false); const [error, setError] = useState(null); const [connectionError, setConnectionError] = useState(null); const [isDisplayReady, setIsDisplayReady] = useState(false); @@ -299,8 +295,6 @@ const GuacamoleAppInner = React.forwardRef< useImperativeHandle(ref, () => ({ disconnect: () => displayRef.current?.disconnect(), isConnected: () => displayRef.current?.isConnected() === true, - openShareModal: () => setShareModalOpen(true), - canShare: () => guacamoleConnectionId !== null, refresh: () => reconnectRef.current(), })); @@ -649,16 +643,16 @@ const GuacamoleAppInner = React.forwardRef< onTouchModeChange={setTouchMode} zoom={displayZoom} onHide={() => setToolbarHidden(true)} - /> - )} - {shareModalOpen && guacamoleConnectionId && ( - setShareModalOpen(false)} - hostId={hostId} - sessionId={guacamoleConnectionId} - protocol={resolvedProtocol} - tabInstanceId={tabId} + slotContext={ + typeof hostId === "number" + ? { + hostId, + sessionId: guacamoleConnectionId, + protocol: resolvedProtocol, + tabInstanceId: tabId, + } + : undefined + } /> )} diff --git a/plugins/remote-desktop/src/frontend/GuacamoleToolbar.tsx b/plugins/remote-desktop/src/frontend/GuacamoleToolbar.tsx index 5a8441f89..990c0181b 100644 --- a/plugins/remote-desktop/src/frontend/GuacamoleToolbar.tsx +++ b/plugins/remote-desktop/src/frontend/GuacamoleToolbar.tsx @@ -32,6 +32,7 @@ import type { GuacamoleTouchMode, } from "./GuacamoleDisplay.tsx"; import { useTranslation } from "@termix/plugin-sdk/frontend"; +import { ActionSlot } from "@termix/plugin-sdk/ui"; import { cn } from "@/lib/utils"; import { detectRuntimeMetaKeyFamily, @@ -50,6 +51,19 @@ interface GuacamoleToolbarProps { zoom?: number; onHide?: () => void; metaKeyFamily?: MetaKeyFamily; + /** Handed to "remote-desktop.toolbar" actions (the share button). */ + slotContext?: RemoteDesktopToolbarContext; +} + +export const REMOTE_DESKTOP_TOOLBAR_SLOT = "remote-desktop.toolbar"; + +/** What a "remote-desktop.toolbar" action is invoked with. */ +export interface RemoteDesktopToolbarContext { + hostId: number; + /** guacd's connection id, once the session is up. */ + sessionId: string | null; + protocol: "rdp" | "vnc" | "telnet"; + tabInstanceId?: string; } const MODIFIER_KEYSYMS = { @@ -140,6 +154,7 @@ export const GuacamoleToolbar: React.FC = ({ zoom = 1, onHide, metaKeyFamily, + slotContext, }) => { const { t } = useTranslation(); const meta = metaKeyLabels( @@ -574,6 +589,16 @@ export const GuacamoleToolbar: React.FC = ({ + {slotContext && ( + [slotContext]} + /> + )} + {/* Collapse */}
diff --git a/plugins/remote-desktop/src/frontend/index.tsx b/plugins/remote-desktop/src/frontend/index.tsx index 88d8df46b..ada84fcdd 100644 --- a/plugins/remote-desktop/src/frontend/index.tsx +++ b/plugins/remote-desktop/src/frontend/index.tsx @@ -27,6 +27,7 @@ import { } from "./HostEditorGuacamoleTabs"; import { getGuacamoleTokenFromHost } from "./guacamole-api"; import { quickConnectGuacHost } from "./quick-connect-guac-host"; +import { REMOTE_DESKTOP_TOOLBAR_SLOT } from "./GuacamoleToolbar.tsx"; type Protocol = "rdp" | "vnc" | "telnet"; @@ -220,6 +221,12 @@ export function activate(app: TermixApp): void { }); if (app.guest) return; + // The share button lives here, contributed by session sharing. + app.declareActionSlot({ + id: REMOTE_DESKTOP_TOOLBAR_SLOT, + accepts: ["button"], + }); + // Mints a token for presenting a host in a collab room. app.registerAction( "session.remoteDisplay.token", diff --git a/plugins/remote-desktop/tests/frontend/GuacamoleToolbar.test.tsx b/plugins/remote-desktop/tests/frontend/GuacamoleToolbar.test.tsx index 3fc72f32b..cd8c80544 100644 --- a/plugins/remote-desktop/tests/frontend/GuacamoleToolbar.test.tsx +++ b/plugins/remote-desktop/tests/frontend/GuacamoleToolbar.test.tsx @@ -4,7 +4,8 @@ import { describe, expect, it, vi } from "vitest"; import { GuacamoleToolbar } from "../../src/frontend/GuacamoleToolbar.js"; import type { GuacamoleDisplayHandle } from "../../src/frontend/GuacamoleDisplay.js"; -vi.mock("react-i18next", () => ({ +vi.mock("react-i18next", async (importOriginal) => ({ + ...(await importOriginal()), useTranslation: () => ({ t: (key: string) => key }), })); diff --git a/plugins/serial/src/frontend/index.tsx b/plugins/serial/src/frontend/index.tsx index c0ccbd6f8..16f8cfe63 100644 --- a/plugins/serial/src/frontend/index.tsx +++ b/plugins/serial/src/frontend/index.tsx @@ -73,9 +73,6 @@ export function activate(app: TermixApp): void { icon: Usb, titleKey: "nav.serial", session: true, - // A physical device attached to this machine, not a remote connection - // another user could join. - shareable: false, // Reconnecting to a COM port automatically after a restart is not a // workflow anyone asked for, and there's no host to restore against // anyway, so a serial tab is a local, ephemeral session like the diff --git a/plugins/session-sharing/CHANGELOG.md b/plugins/session-sharing/CHANGELOG.md new file mode 100644 index 000000000..8a1c198dd --- /dev/null +++ b/plugins/session-sharing/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +## 1.0.0 + +Bundled with Termix 2.9.0. + +- Session sharing and collaboration rooms moved out of core and now serve + under `/plugin-api/session-sharing`. The old `/session-sharing` and + `/collab` routes are gone; guest links (`?view=shared` and + `?view=collab-guest`) keep working. +- Owns its data: core's `session_shares`, `session_share_participants`, + `collab_rooms` and `collab_room_members` tables are renamed into this + plugin on first activation. +- The instance-wide sharing switch and each host's sharing switch moved into + this plugin's admin and host settings. +- New `session-sharing.use` permission, given to the admin and user roles. +- Provides `sessions.sharing` and the `sessions.sharing.guests` registry entry, + which the terminal uses for member and guest joins. +- The share button moved from the tab bar into the terminal and remote + desktop toolbars. diff --git a/plugins/session-sharing/README.md b/plugins/session-sharing/README.md new file mode 100644 index 000000000..4b124953c --- /dev/null +++ b/plugins/session-sharing/README.md @@ -0,0 +1,31 @@ +# Session Sharing + +Share a live terminal or remote desktop session by link or with another user, +and present sessions to a group in collaboration rooms. + +Bundled with Termix and enabled by default. It imports only +`@termix/plugin-sdk`. + +## What it does + +- **Shares**: a link for anonymous guests, or a grant for one user, read-only + or read-write, with an expiry. The share button sits in the terminal and + remote desktop toolbars. +- **Rooms**: members watch one stage, the live session the presenter shows. + The presenter or host can hand input control to a member, and a room can + have an anonymous guest link. +- **`sessions.sharing`**: member joins and room events for the terminal + socket. Guest joins go through `sessions.sharing.guests` on `ctx.registry`, + because a guest has no user for a service's permission check. + +It reaches live sessions only through the `sessions.live` service, keyed by +session type: ssh-terminal provides `ssh`, remote desktop provides `rdp`, +`vnc` and `telnet`. + +## Layout + + manifest.json id, capabilities and what this plugin contributes + src/backend/ activate(ctx), routes, the room hub, the services + src/frontend/ share dialog, guest views, rooms panel and tab + migrations/ one .sql per dialect, generated from src/backend/tables.ts + tests/ backend (node) and frontend (jsdom) diff --git a/plugins/session-sharing/locales/en.json b/plugins/session-sharing/locales/en.json new file mode 100644 index 000000000..49540e3b4 --- /dev/null +++ b/plugins/session-sharing/locales/en.json @@ -0,0 +1,160 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Connecting to shared session...", + "linkInvalid": "This share link is invalid, expired, or has been revoked", + "rateLimited": "Too many attempts, please try again shortly", + "sessionEnded": "This session has ended", + "readOnlyBadge": "View only", + "ownerLabel": "Host owner" + }, + "modalTitle": "Share session", + "shareButton": "Share", + "notReadyToShare": "Session is not ready to share yet", + "modeTab": { + "link": "Link", + "user": "User" + }, + "linkModeDescription": "Anyone with this link can join, no account required.", + "userModeDescription": "Share with a specific user who already has access to this host. If they do not have access yet, share the host with them first or use a link instead. Once shared, the session appears in their Connections tab.", + "permissionLevel": { + "label": "Permission level", + "readOnly": "Read-only", + "readOnlyDescription": "Can watch the session live but cannot type or interact.", + "readWrite": "Read-write", + "readWriteDescription": "Can type and interact with the session just like the owner." + }, + "expiryLabel": "Link expiry", + "createLinkButton": "Create link", + "createShareButton": "Share with user", + "searchUsersPlaceholder": "Search users...", + "noUsersFound": "No users found", + "linkCreated": "Share link created", + "linkCopied": "Link copied to clipboard", + "copyLink": "Copy link", + "shareCreated": "Session shared. It will appear in their Connections tab.", + "shareFailed": "Failed to create share", + "userLacksHostAccess": "That user does not have access to this host yet. Share the host with them first, or use a link instead.", + "activeShares": "Active shares", + "noActiveShares": "No active shares for this session", + "revoke": "Revoke", + "revokeConfirmTitle": "Revoke this share?", + "revokeConfirmDescription": "Anyone using this share will lose access immediately.", + "revoked": "Share revoked", + "revokeFailed": "Failed to revoke share", + "joinCount": "{{count}} join", + "joinCount_other": "{{count}} joins", + "expiresAt": "Expires {{date}}", + "linkShareBadge": "Link", + "userShareBadge": "User: {{username}}", + "loadSharesFailed": "Failed to load active shares", + "remoteDisplayUnavailable": "Remote desktop is not available right now.", + "sharedWithMe": "Sessions shared with me", + "customHoursPlaceholder": "Hours until access expires" + }, + "collab": { + "createRoom": "New room", + "roomName": "Room name", + "persistentRoom": "Persistent room", + "persistentRoomHint": "Persistent rooms stay listed after the meeting ends and can be reused.", + "noRooms": "No rooms yet. Create one to start a meeting.", + "openRoom": "Open", + "endRoom": "End meeting", + "leaveRoom": "Leave", + "deleteMember": "Remove", + "removeMember": "Remove {{name}}", + "removeMemberTitle": "Remove this member?", + "removeMemberDescription": "{{name}} will lose access to this room immediately. Any active SSH viewer will be disconnected.", + "invite": "Invite", + "inviteTitle": "Invite members", + "members": "Members", + "membersPanel": "Room members and control requests", + "membersWithCount": "Members ({{count}})", + "online": "online", + "offline": "offline", + "you": "(you)", + "hostBadge": "Host", + "presenterBadge": "Presenting", + "present": "Present", + "presentTitle": "Take the stage", + "presentHost": "Host", + "searchHosts": "Search hosts", + "noHostsFound": "No available hosts found.", + "presentProtocol": "Protocol", + "stopPresenting": "Stop presenting", + "takeOver": "Take over", + "takeOverConfirmTitle": "Replace the current presentation?", + "takeOverDescription": "{{name}} is presenting now. Taking over will disconnect everyone from that stage.", + "emptyStage": "Nobody is presenting. Take the stage to share a session.", + "stageLoading": "Connecting to the stage...", + "stageEnded": "The presentation ended", + "roomEnded": "This meeting has ended", + "presenterLabel": "{{name}} is presenting", + "guestLink": "Guest link", + "guestLinkOn": "Guest link is on. Anyone with the link can watch the stage.", + "guestLinkOff": "Guest link is off.", + "copyLink": "Copy link", + "rotateLink": "Rotate link", + "disableLink": "Disable link", + "rotateLinkConfirmTitle": "Rotate the guest link?", + "rotateLinkDescription": "The current link will stop working immediately. Connected SSH guests will be disconnected.", + "disableLinkConfirmTitle": "Disable the guest link?", + "disableLinkDescription": "Anonymous SSH viewers will be disconnected immediately and the current link will stop working.", + "linkCopied": "Link copied", + "roles": "Roles", + "users": "Users", + "searchUsers": "Search users", + "endConfirmTitle": "End this meeting?", + "endDescription": "{{name}} will end for everyone and cannot be reopened.", + "endPersistentDescription": "The active presentation in {{name}} will stop. The room remains available for reuse.", + "deleteRoom": "Delete room", + "deleteConfirmTitle": "Delete this room?", + "deleteDescription": "{{name}} will be permanently deleted for everyone and cannot be undone.", + "invitedTo": "You were invited to \"{{name}}\"", + "guest": { + "title": "Meeting", + "waiting": "Waiting for the presenter...", + "linkInvalid": "This guest link is invalid or the meeting has ended" + }, + "reopenFromPanel": "This meeting tab expired. Reopen the room from the Meetings panel.", + "requestControl": "Request control", + "cancelControlRequest": "Cancel request", + "controlQueue": "Control requests", + "noControlRequests": "No pending requests.", + "grantControlTo": "Give control to {{name}}", + "dismissControlRequest": "Dismiss {{name}}'s control request", + "releaseControl": "Release control", + "grantControl": "Give control", + "revokeControl": "Take back control", + "controlRequestedBy": "{{name}} asked for control", + "grant": "Grant", + "controlBadge": "In control", + "youArePresenting": "You are presenting in another tab. Stop presenting to hand the stage over.", + "created": "Room created", + "invited": "Members invited", + "roomTab": "Meeting" + }, + "nav": { + "collab": "Meetings" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Allow Session Sharing", + "description": "Allow live terminal, RDP, VNC, and Telnet sessions to be shared instance-wide. Overrides every per-host sharing toggle when disabled." + } + }, + "host": { + "allowSessionSharing": { + "label": "Allow Session Sharing", + "description": "Let live sessions on this host be shared via link or with other users" + } + } + }, + "permissions": { + "use": { + "title": "Share sessions and join rooms", + "description": "Share live sessions by link or with other users, and create and join collaboration rooms." + } + } +} diff --git a/plugins/session-sharing/locales/translated/af_ZA.json b/plugins/session-sharing/locales/translated/af_ZA.json new file mode 100644 index 000000000..f8cfe77a8 --- /dev/null +++ b/plugins/session-sharing/locales/translated/af_ZA.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Verbind met gedeelde sessie...", + "linkInvalid": "Hierdie deelskakel is ongeldig, het verval of is herroep", + "rateLimited": "Te veel pogings, probeer asseblief weer binnekort", + "sessionEnded": "Hierdie sessie is verby", + "readOnlyBadge": "Slegs kyk", + "ownerLabel": "Gasheer-eienaar" + }, + "modalTitle": "Deel sessie", + "shareButton": "Deel", + "notReadyToShare": "Sessie is nog nie gereed om te deel nie", + "modeTab": { + "link": "Skakel", + "user": "Gebruiker" + }, + "linkModeDescription": "Enigiemand met hierdie skakel kan aansluit, geen rekening nodig nie.", + "userModeDescription": "Deel met 'n spesifieke gebruiker wat reeds toegang tot hierdie gasheer het. Indien hulle nog nie toegang het nie, deel die gasheer eers met hulle of gebruik eerder 'n skakel. Sodra dit gedeel is, verskyn die sessie in hul Verbindings-oortjie.", + "permissionLevel": { + "label": "Toestemmingsvlak", + "readOnly": "Leesalleen", + "readOnlyDescription": "Kan die sessie regstreeks kyk, maar kan nie tik of interaksie hê nie.", + "readWrite": "Lees-skryf", + "readWriteDescription": "Kan tik en met die sessie interaksie hê net soos die eienaar." + }, + "expiryLabel": "Skakelvervaldatum", + "createLinkButton": "Skep skakel", + "createShareButton": "Deel met gebruiker", + "searchUsersPlaceholder": "Soek gebruikers...", + "noUsersFound": "Geen gebruikers gevind nie", + "linkCreated": "Deel skakel geskep", + "linkCopied": "Skakel gekopieer na knipbord", + "copyLink": "Kopieer skakel", + "shareCreated": "Sessie gedeel. Dit sal in hul Verbindings-oortjie verskyn.", + "shareFailed": "Kon nie deel skep nie", + "userLacksHostAccess": "Daardie gebruiker het nog nie toegang tot hierdie gasheer nie. Deel die gasheer eers met hulle, of gebruik eerder 'n skakel.", + "activeShares": "Aktiewe aandele", + "noActiveShares": "Geen aktiewe delings vir hierdie sessie nie", + "revoke": "Herroep", + "revokeConfirmTitle": "Herroep hierdie deling?", + "revokeConfirmDescription": "Enigiemand wat hierdie deel gebruik, sal onmiddellik toegang verloor.", + "revoked": "Deel herroep", + "revokeFailed": "Kon nie deel herroep nie", + "joinCount": "{{count}} sluit aan", + "joinCount_other": "{{count}} sluit aan", + "expiresAt": "Vervaldatum {{date}}", + "linkShareBadge": "Skakel", + "userShareBadge": "Gebruiker: {{username}}", + "loadSharesFailed": "Kon nie aktiewe delings laai nie", + "customHoursPlaceholder": "Ure totdat toegang verval" + }, + "collab": { + "createRoom": "Nuwe kamer", + "roomName": "Kamernaam", + "persistentRoom": "Permanente kamer", + "persistentRoomHint": "Permanente kamers bly gelys nadat die vergadering eindig en kan hergebruik word.", + "noRooms": "Geen kamers nog nie. Skep een om 'n vergadering te begin.", + "openRoom": "Oop", + "endRoom": "Beëindig vergadering", + "leaveRoom": "Vertrek", + "deleteMember": "Verwyder", + "removeMember": "Verwyder {{name}}", + "removeMemberTitle": "Verwyder hierdie lid?", + "removeMemberDescription": "{{name}} sal onmiddellik toegang tot hierdie kamer verloor. Enige aktiewe SSH-kyker sal ontkoppel word.", + "invite": "Nooi uit", + "inviteTitle": "Nooi lede uit", + "members": "Lede", + "membersPanel": "Kamerlede en beheerversoeke", + "membersWithCount": "Lede ({{count}})", + "online": "aanlyn", + "offline": "vanlyn", + "you": "(jy)", + "hostBadge": "Gasheer", + "presenterBadge": "Aanbieding", + "present": "Aanwesig", + "presentTitle": "Neem die verhoog", + "presentHost": "Gasheer", + "searchHosts": "Soek gashere", + "noHostsFound": "Geen beskikbare gashere gevind nie.", + "presentProtocol": "Protokol", + "stopPresenting": "Hou op met aanbied", + "takeOver": "Neem oor", + "takeOverConfirmTitle": "Vervang die huidige aanbieding?", + "takeOverDescription": "{{name}} bied nou aan. Oorname sal almal van daardie stadium ontkoppel.", + "emptyStage": "Niemand bied aan nie. Neem die verhoog om 'n sessie te deel.", + "stageLoading": "Verbind met die verhoog...", + "stageEnded": "Die aanbieding het geëindig", + "roomEnded": "Hierdie vergadering het geëindig", + "presenterLabel": "{{name}} bied aan", + "guestLink": "Gas skakel", + "guestLinkOn": "Gasskakel is aan. Enigiemand met die skakel kan die verhoog kyk.", + "guestLinkOff": "Gas skakel is af.", + "copyLink": "Kopieer skakel", + "rotateLink": "Roteer skakel", + "disableLink": "Deaktiveer skakel", + "rotateLinkConfirmTitle": "Draai die gasskakel?", + "rotateLinkDescription": "Die huidige skakel sal onmiddellik ophou werk. Gekoppelde SSH-gaste sal ontkoppel word.", + "disableLinkConfirmTitle": "Deaktiveer die gasskakel?", + "disableLinkDescription": "Anonieme SSH-kykers sal onmiddellik ontkoppel word en die huidige skakel sal ophou werk.", + "linkCopied": "Skakel gekopieer", + "roles": "Rolle", + "users": "Gebruikers", + "searchUsers": "Soek gebruikers", + "endConfirmTitle": "Beëindig hierdie vergadering?", + "endDescription": "{{name}} sal vir almal eindig en kan nie weer heropen word nie.", + "endPersistentDescription": "Die aktiewe aanbieding in {{name}} sal stop. Die kamer bly beskikbaar vir hergebruik.", + "deleteRoom": "Vee kamer uit", + "deleteConfirmTitle": "Vee hierdie kamer uit?", + "deleteDescription": "{{name}} sal permanent vir almal uitgevee word en kan nie ongedaan gemaak word nie.", + "invitedTo": "Jy is genooi na \"{{name}}\"", + "guest": { + "title": "Vergadering", + "waiting": "Wag vir die aanbieder...", + "linkInvalid": "Hierdie gasskakel is ongeldig of die vergadering het geëindig" + }, + "reopenFromPanel": "Hierdie vergadering-oortjie het verval. Maak die kamer weer oop vanaf die Vergaderings-paneel.", + "requestControl": "Versoekbeheer", + "cancelControlRequest": "Kanselleer versoek", + "controlQueue": "Beheerversoeke", + "noControlRequests": "Geen hangende versoeke nie.", + "grantControlTo": "Gee beheer aan {{name}}", + "dismissControlRequest": "Maak {{name}}se beheerversoek toe", + "releaseControl": "Vrystellingsbeheer", + "grantControl": "Gee beheer", + "revokeControl": "Neem beheer terug", + "controlRequestedBy": "{{name}} het om beheer gevra", + "grant": "Toekenning", + "controlBadge": "In beheer", + "youArePresenting": "Jy bied aan in 'n ander oortjie. Stop die aanbieding om die verhoog oor te dra.", + "created": "Kamer geskep", + "invited": "Lede genooi", + "roomTab": "Vergadering" + }, + "nav": { + "collab": "Vergaderings" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Laat Sessiedeling Toe", + "description": "Laat lewendige terminaal-, RDP-, VNC- en Telnet-sessies toe om instansiewyd gedeel te word. Oorskryf elke deelskakelaar per gasheer wanneer dit gedeaktiveer is." + } + }, + "host": { + "allowSessionSharing": { + "label": "Laat Sessiedeling Toe", + "description": "Laat regstreekse sessies op hierdie gasheer via skakel of met ander gebruikers gedeel word" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/ar_SA.json b/plugins/session-sharing/locales/translated/ar_SA.json new file mode 100644 index 000000000..c79b35711 --- /dev/null +++ b/plugins/session-sharing/locales/translated/ar_SA.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "جارٍ الاتصال بالجلسة المشتركة...", + "linkInvalid": "رابط المشاركة هذا غير صالح أو منتهي الصلاحية أو تم إلغاؤه", + "rateLimited": "عدد المحاولات كبير جدًا، يرجى المحاولة مرة أخرى قريبًا", + "sessionEnded": "انتهت هذه الجلسة", + "readOnlyBadge": "للعرض فقط", + "ownerLabel": "مالك المضيف" + }, + "modalTitle": "جلسة مشاركة", + "shareButton": "يشارك", + "notReadyToShare": "الجلسة غير جاهزة للمشاركة بعد", + "modeTab": { + "link": "وصلة", + "user": "مستخدم" + }, + "linkModeDescription": "يمكن لأي شخص لديه هذا الرابط الانضمام، ولا يلزم وجود حساب.", + "userModeDescription": "شارك الملف مع مستخدم محدد لديه صلاحية الوصول إلى هذا المضيف. إذا لم يكن لديه صلاحية الوصول بعد، فشارك معه المضيف أولاً أو استخدم رابطًا. بمجرد المشاركة، ستظهر الجلسة في علامة تبويب \"الاتصالات\" الخاصة به.", + "permissionLevel": { + "label": "مستوى الإذن", + "readOnly": "للقراءة فقط", + "readOnlyDescription": "يمكنني مشاهدة الجلسة مباشرة ولكن لا يمكنني الكتابة أو التفاعل.", + "readWrite": "القراءة والكتابة", + "readWriteDescription": "يمكنه الكتابة والتفاعل مع الجلسة تمامًا مثل المالك." + }, + "expiryLabel": "انتهاء صلاحية الرابط", + "createLinkButton": "إنشاء رابط", + "createShareButton": "مشاركة مع المستخدم", + "searchUsersPlaceholder": "ابحث عن المستخدمين...", + "noUsersFound": "لم يتم العثور على مستخدمين", + "linkCreated": "تم إنشاء رابط المشاركة", + "linkCopied": "تم نسخ الرابط إلى الحافظة", + "copyLink": "نسخ الوصلة", + "shareCreated": "تمت مشاركة الجلسة. ستظهر في علامة تبويب \"الاتصالات\" الخاصة بهم.", + "shareFailed": "فشل إنشاء المشاركة", + "userLacksHostAccess": "لا يملك هذا المستخدم صلاحية الوصول إلى هذا المضيف بعد. شارك المضيف معه أولاً، أو استخدم رابطاً بدلاً من ذلك.", + "activeShares": "الأسهم النشطة", + "noActiveShares": "لا توجد أسهم نشطة لهذه الجلسة", + "revoke": "إلغاء", + "revokeConfirmTitle": "إلغاء هذه الحصة؟", + "revokeConfirmDescription": "أي شخص يستخدم هذا الملف المشترك سيفقد الوصول إليه فوراً.", + "revoked": "تم إلغاء الحصة", + "revokeFailed": "فشل في إلغاء المشاركة", + "joinCount": "{{count}} انضم", + "joinCount_other": "{{count}} ينضم", + "expiresAt": "تاريخ الانتهاء {{date}}", + "linkShareBadge": "وصلة", + "userShareBadge": "المستخدم: {{username}}", + "loadSharesFailed": "فشل تحميل المشاركات النشطة", + "customHoursPlaceholder": "عدد الساعات حتى انتهاء صلاحية الوصول" + }, + "collab": { + "createRoom": "غرف جديدة", + "roomName": "اسم الغرفة", + "persistentRoom": "غرفة مستمرة", + "persistentRoomHint": "تبقى الغرف الدائمة مدرجة بعد انتهاء الاجتماع ويمكن إعادة استخدامها.", + "noRooms": "لا توجد غرف اجتماعات بعد. أنشئ غرفة لبدء اجتماع.", + "openRoom": "يفتح", + "endRoom": "انتهاء الاجتماع", + "leaveRoom": "يترك", + "deleteMember": "يزيل", + "removeMember": "إزالة {{name}}", + "removeMemberTitle": "هل تريد حذف هذا العضو؟", + "removeMemberDescription": "سيفقد المستخدم {{name}} إمكانية الوصول إلى هذه الغرفة فورًا. وسيتم فصل أي برنامج عرض SSH نشط.", + "invite": "يدعو", + "inviteTitle": "دعوة الأعضاء", + "members": "أعضاء", + "membersPanel": "أعضاء الغرفة وطلبات التحكم", + "membersWithCount": "الأعضاء ({{count}})", + "online": "متصل", + "offline": "غير متصل بالإنترنت", + "you": "(أنت)", + "hostBadge": "يستضيف", + "presenterBadge": "تقديم", + "present": "حاضر", + "presentTitle": "اصعد إلى المسرح", + "presentHost": "يستضيف", + "searchHosts": "البحث عن المضيفين", + "noHostsFound": "لم يتم العثور على مضيفين متاحين.", + "presentProtocol": "بروتوكول", + "stopPresenting": "توقف عن التقديم", + "takeOver": "تولي زمام الأمور", + "takeOverConfirmTitle": "هل تريد استبدال العرض التقديمي الحالي؟", + "takeOverDescription": "{{name}} يقدم العرض الآن. سيؤدي الاستحواذ على المنصة إلى فصل الجميع عنها.", + "emptyStage": "لا يوجد مقدمون. تفضلوا بالصعود إلى المنصة لمشاركة جلسة.", + "stageLoading": "الاتصال بالمسرح...", + "stageEnded": "انتهى العرض التقديمي", + "roomEnded": "انتهى هذا الاجتماع", + "presenterLabel": "{{name}} يقدم", + "guestLink": "رابط الضيف", + "guestLinkOn": "رابط الضيوف مُفعّل. يمكن لأي شخص لديه الرابط مشاهدة العرض.", + "guestLinkOff": "رابط الضيف معطل.", + "copyLink": "نسخ الوصلة", + "rotateLink": "تدوير الرابط", + "disableLink": "تعطيل الرابط", + "rotateLinkConfirmTitle": "هل تريد تدوير رابط الضيف؟", + "rotateLinkDescription": "سيتوقف الرابط الحالي عن العمل فوراً. سيتم فصل الضيوف المتصلين عبر SSH.", + "disableLinkConfirmTitle": "هل تريد تعطيل رابط الضيف؟", + "disableLinkDescription": "سيتم قطع اتصال عارضي SSH المجهولين على الفور وسيتوقف الرابط الحالي عن العمل.", + "linkCopied": "تم نسخ الرابط", + "roles": "الأدوار", + "users": "المستخدمون", + "searchUsers": "ابحث عن المستخدمين", + "endConfirmTitle": "هل ننهي هذا الاجتماع؟", + "endDescription": "سينتهي {{name}} للجميع ولن يكون من الممكن إعادة فتحه.", + "endPersistentDescription": "سيتوقف العرض التقديمي النشط في {{name}} . وستبقى الغرفة متاحة لإعادة الاستخدام.", + "deleteRoom": "حذف الغرفة", + "deleteConfirmTitle": "هل تريد حذف هذه الغرفة؟", + "deleteDescription": "سيتم حذف {{name}} نهائياً للجميع ولا يمكن التراجع عنه.", + "invitedTo": "لقد تمت دعوتك إلى \"{{name}}\"", + "guest": { + "title": "مقابلة", + "waiting": "في انتظار المذيع...", + "linkInvalid": "رابط الضيف هذا غير صالح أو انتهى الاجتماع" + }, + "reopenFromPanel": "انتهت صلاحية علامة تبويب هذا الاجتماع. أعد فتح الغرفة من لوحة الاجتماعات.", + "requestControl": "التحكم في الطلبات", + "cancelControlRequest": "إلغاء الطلب", + "controlQueue": "طلبات التحكم", + "noControlRequests": "لا توجد طلبات معلقة.", + "grantControlTo": "قم بمنح التحكم إلى {{name}}", + "dismissControlRequest": "رفض طلب التحكم {{name}}", + "releaseControl": "التحكم في الإصدار", + "grantControl": "امنح السيطرة", + "revokeControl": "استعد السيطرة", + "controlRequestedBy": "طلب {{name}} التحكم", + "grant": "منحة", + "controlBadge": "مسيطر", + "youArePresenting": "أنت تعرض المحتوى في علامة تبويب أخرى. توقف عن العرض لتسليم المنصة.", + "created": "تم إنشاء الغرفة", + "invited": "الأعضاء المدعوون", + "roomTab": "مقابلة" + }, + "nav": { + "collab": "الاجتماعات" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "السماح بمشاركة الجلسة", + "description": "يسمح هذا الخيار بمشاركة جلسات الطرفية المباشرة، وجلسات RDP، وVNC، وTelnet على مستوى الجهاز. ويتجاوز هذا الخيار جميع إعدادات المشاركة الخاصة بكل مضيف عند تعطيله." + } + }, + "host": { + "allowSessionSharing": { + "label": "السماح بمشاركة الجلسة", + "description": "يمكن مشاركة الجلسات المباشرة على هذا المضيف عبر رابط أو مع مستخدمين آخرين" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/bg_BG.json b/plugins/session-sharing/locales/translated/bg_BG.json new file mode 100644 index 000000000..d5409b0e7 --- /dev/null +++ b/plugins/session-sharing/locales/translated/bg_BG.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Свързване със споделена сесия...", + "linkInvalid": "Този линк за споделяне е невалиден, изтекъл или е отменен", + "rateLimited": "Твърде много опити, моля опитайте отново скоро", + "sessionEnded": "Тази сесия приключи", + "readOnlyBadge": "Само преглед", + "ownerLabel": "Собственик на домакин" + }, + "modalTitle": "Споделяне на сесия", + "shareButton": "Споделяне", + "notReadyToShare": "Сесията все още не е готова за споделяне", + "modeTab": { + "link": "Връзка", + "user": "Потребител" + }, + "linkModeDescription": "Всеки с този линк може да се присъедини, не е необходим акаунт.", + "userModeDescription": "Споделете с конкретен потребител, който вече има достъп до този хост. Ако все още няма достъп, първо споделете хоста с него или използвайте връзка. След като бъде споделена, сесията се показва в раздела му „Връзки“.", + "permissionLevel": { + "label": "Ниво на разрешение", + "readOnly": "Само за четене", + "readOnlyDescription": "Може да гледа сесията на живо, но не може да пише или да взаимодейства.", + "readWrite": "Четене и запис", + "readWriteDescription": "Може да пише и да взаимодейства със сесията точно като собственика." + }, + "expiryLabel": "Изтичане на срока на валидност на връзката", + "createLinkButton": "Създаване на връзка", + "createShareButton": "Споделяне с потребителя", + "searchUsersPlaceholder": "Търсене на потребители...", + "noUsersFound": "Не са намерени потребители", + "linkCreated": "Линкът за споделяне е създаден", + "linkCopied": "Връзката е копирана в буферната памет", + "copyLink": "Копиране на връзката", + "shareCreated": "Сесията е споделена. Тя ще се покаже в раздела им „Връзки“.", + "shareFailed": "Създаването на споделено съдържание не бе успешно", + "userLacksHostAccess": "Тозият потребител все още няма достъп до този хост. Първо споделете хоста с него или използвайте линк.", + "activeShares": "Активни акции", + "noActiveShares": "Няма активни споделяния за тази сесия", + "revoke": "Отмяна", + "revokeConfirmTitle": "Да се отмени ли това споделяне?", + "revokeConfirmDescription": "Всеки, който използва това споделяне, ще загуби достъп незабавно.", + "revoked": "Акцията е отменена", + "revokeFailed": "Неуспешно отменяне на споделянето", + "joinCount": "{{count}} присъединяване", + "joinCount_other": "{{count}} се присъединява", + "expiresAt": "Изтича {{date}}", + "linkShareBadge": "Връзка", + "userShareBadge": "Потребител: {{username}}", + "loadSharesFailed": "Зареждането на активните споделяния не бе успешно", + "customHoursPlaceholder": "Часове до изтичане на срока на достъп" + }, + "collab": { + "createRoom": "Нова стая", + "roomName": "Име на стаята", + "persistentRoom": "Постоянна стая", + "persistentRoomHint": "Постоянните стаи остават в списъка след края на срещата и могат да бъдат използвани повторно.", + "noRooms": "Все още няма стаи. Създайте една, за да започнете среща.", + "openRoom": "Отворено", + "endRoom": "Край на срещата", + "leaveRoom": "Оставете", + "deleteMember": "Премахване", + "removeMember": "Премахване на {{name}}", + "removeMemberTitle": "Да се премахне ли този член?", + "removeMemberDescription": "{{name}} ще загуби достъп до тази стая незабавно. Всеки активен SSH зрител ще бъде прекъснат.", + "invite": "Покани", + "inviteTitle": "Поканете членове", + "members": "Членове", + "membersPanel": "Членове на стаята и заявки за контрол", + "membersWithCount": "Членове ({{count}})", + "online": "онлайн", + "offline": "офлайн", + "you": "(ти)", + "hostBadge": "Домакин", + "presenterBadge": "Представяне", + "present": "Настояще", + "presentTitle": "Излезте на сцената", + "presentHost": "Домакин", + "searchHosts": "Търсене на хостове", + "noHostsFound": "Няма намерени налични хостове.", + "presentProtocol": "Протокол", + "stopPresenting": "Спиране на представянето", + "takeOver": "Поемете контрола", + "takeOverConfirmTitle": "Да се замени ли текущата презентация?", + "takeOverDescription": "{{name}} представя сега. Поемането на водещия ще изключи всички от тази сцена.", + "emptyStage": "Никой не представя. Излезте на сцената, за да споделите сесия.", + "stageLoading": "Свързване със сцената...", + "stageEnded": "Презентацията приключи", + "roomEnded": "Тази среща приключи", + "presenterLabel": "{{name}} представя", + "guestLink": "Връзка за гости", + "guestLinkOn": "Връзката за гости е включена. Всеки с връзката може да гледа сцената.", + "guestLinkOff": "Връзката за гости е изключена.", + "copyLink": "Копиране на връзката", + "rotateLink": "Завъртане на връзката", + "disableLink": "Деактивиране на връзката", + "rotateLinkConfirmTitle": "Да се завърти ли линкът за гости?", + "rotateLinkDescription": "Текущата връзка ще спре да работи незабавно. Свързаните SSH гости ще бъдат прекъснати.", + "disableLinkConfirmTitle": "Да се деактивира ли връзката за гости?", + "disableLinkDescription": "Анонимните SSH зрители ще бъдат незабавно прекъснати и текущата връзка ще спре да работи.", + "linkCopied": "Връзката е копирана", + "roles": "Роли", + "users": "Потребители", + "searchUsers": "Търсене на потребители", + "endConfirmTitle": "Да се прекрати ли тази среща?", + "endDescription": "{{name}} ще приключи за всички и не може да бъде отворено отново.", + "endPersistentDescription": "Активната презентация в {{name}} ще спре. Стаята остава достъпна за повторна употреба.", + "deleteRoom": "Изтриване на стая", + "deleteConfirmTitle": "Да се изтрие ли тази стая?", + "deleteDescription": "{{name}} ще бъде изтрито за постоянно за всички и действието не може да бъде отменено.", + "invitedTo": "Поканиха ви в „{{name}}“", + "guest": { + "title": "Среща", + "waiting": "В очакване на водещия...", + "linkInvalid": "Този линк за гости е невалиден или срещата е приключила" + }, + "reopenFromPanel": "Този раздел за среща е с изтекъл срок. Отворете отново стаята от панела „Срещи“.", + "requestControl": "Заявка за контрол", + "cancelControlRequest": "Отмяна на заявката", + "controlQueue": "Заявки за контрол", + "noControlRequests": "Няма чакащи заявки.", + "grantControlTo": "Дайте контрол на {{name}}", + "dismissControlRequest": "Отхвърляне на заявката за контрол на {{name}}", + "releaseControl": "Контрол на освобождаването", + "grantControl": "Дайте контрол", + "revokeControl": "Върнете си контрола", + "controlRequestedBy": "{{name}} поиска контрол", + "grant": "Грант", + "controlBadge": "В контрол", + "youArePresenting": "Презентирате в друг раздел. Спрете презентацията, за да предадете сцената.", + "created": "Стаята е създадена", + "invited": "Поканени членове", + "roomTab": "Среща" + }, + "nav": { + "collab": "Срещи" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Разрешаване на споделяне на сесия", + "description": "Разрешава споделянето на сесии на живо на терминал, RDP, VNC и Telnet в целия екземпляр. Заменя всяко превключвател за споделяне на хост, когато е деактивиран." + } + }, + "host": { + "allowSessionSharing": { + "label": "Разрешаване на споделяне на сесия", + "description": "Позволете споделянето на сесиите на живо на този хост чрез линк или с други потребители" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/bn_BD.json b/plugins/session-sharing/locales/translated/bn_BD.json new file mode 100644 index 000000000..5d3c417ad --- /dev/null +++ b/plugins/session-sharing/locales/translated/bn_BD.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "শেয়ার করা সেশনে সংযোগ স্থাপন করা হচ্ছে...", + "linkInvalid": "এই শেয়ার লিঙ্কটি অবৈধ, মেয়াদোত্তীর্ণ, বা বাতিল করা হয়েছে।", + "rateLimited": "অনেকবার চেষ্টা করা হয়েছে, অনুগ্রহ করে কিছুক্ষণ পরে আবার চেষ্টা করুন।", + "sessionEnded": "এই অধিবেশন শেষ হয়েছে", + "readOnlyBadge": "শুধুমাত্র দেখার জন্য", + "ownerLabel": "হোস্ট মালিক" + }, + "modalTitle": "সেশন শেয়ার করুন", + "shareButton": "শেয়ার", + "notReadyToShare": "সেশনটি এখনও শেয়ার করার জন্য প্রস্তুত নয়।", + "modeTab": { + "link": "লিঙ্ক", + "user": "ব্যবহারকারী" + }, + "linkModeDescription": "এই লিঙ্কের মাধ্যমে যে কেউ যোগ দিতে পারবেন, কোনো অ্যাকাউন্টের প্রয়োজন নেই।", + "userModeDescription": "এমন একজন নির্দিষ্ট ব্যবহারকারীর সাথে শেয়ার করুন যার এই হোস্টে আগে থেকেই অ্যাক্সেস আছে। যদি তাদের এখনও অ্যাক্সেস না থাকে, তাহলে প্রথমে তাদের সাথে হোস্টটি শেয়ার করুন অথবা এর পরিবর্তে একটি লিঙ্ক ব্যবহার করুন। শেয়ার করা হয়ে গেলে, সেশনটি তাদের 'কানেকশনস' ট্যাবে দেখা যাবে।", + "permissionLevel": { + "label": "অনুমতি স্তর", + "readOnly": "শুধুমাত্র পঠনযোগ্য", + "readOnlyDescription": "সেশনটি সরাসরি দেখা যাবে কিন্তু টাইপ বা অংশগ্রহণ করা যাবে না।", + "readWrite": "পড়া-লেখা", + "readWriteDescription": "মালিকের মতোই সেশনে টাইপ করতে এবং ইন্টারঅ্যাক্ট করতে পারবেন।" + }, + "expiryLabel": "লিঙ্কের মেয়াদ শেষ", + "createLinkButton": "লিঙ্ক তৈরি করুন", + "createShareButton": "ব্যবহারকারীর সাথে শেয়ার করুন", + "searchUsersPlaceholder": "ব্যবহারকারীদের অনুসন্ধান করুন...", + "noUsersFound": "কোন ব্যবহারকারী খুঁজে পাওয়া যায়নি", + "linkCreated": "শেয়ার লিঙ্ক তৈরি করা হয়েছে", + "linkCopied": "লিঙ্ক ক্লিপবোর্ডে কপি করা হয়েছে", + "copyLink": "লিঙ্ক কপি করুন", + "shareCreated": "সেশনটি শেয়ার করা হয়েছে। এটি তাদের কানেকশনস ট্যাবে দেখা যাবে।", + "shareFailed": "শেয়ার তৈরি করতে ব্যর্থ হয়েছে", + "userLacksHostAccess": "ওই ব্যবহারকারীর এখনও এই হোস্টে অ্যাক্সেস নেই। প্রথমে তাদের সাথে হোস্টটি শেয়ার করুন, অথবা এর পরিবর্তে একটি লিঙ্ক ব্যবহার করুন।", + "activeShares": "সক্রিয় শেয়ার", + "noActiveShares": "এই সেশনের জন্য কোনো সক্রিয় শেয়ার নেই", + "revoke": "বাতিল করুন", + "revokeConfirmTitle": "এই শেয়ারটি প্রত্যাহার করবেন?", + "revokeConfirmDescription": "যে কেউ এই শেয়ারটি ব্যবহার করলে, তিনি অবিলম্বে অ্যাক্সেস হারাবেন।", + "revoked": "শেয়ার প্রত্যাহার করা হয়েছে", + "revokeFailed": "শেয়ার প্রত্যাহার করতে ব্যর্থ হয়েছে", + "joinCount": "{{count}} যোগদান করুন", + "joinCount_other": "{{count}} যোগদান করে", + "expiresAt": "মেয়াদ শেষ হবে {{date}}", + "linkShareBadge": "লিঙ্ক", + "userShareBadge": "ব্যবহারকারী: {{username}}", + "loadSharesFailed": "সক্রিয় শেয়ারগুলি লোড করতে ব্যর্থ হয়েছে", + "customHoursPlaceholder": "অ্যাক্সেসের মেয়াদ শেষ হতে বাকি ঘন্টা" + }, + "collab": { + "createRoom": "নতুন ঘর", + "roomName": "রুমের নাম", + "persistentRoom": "স্থায়ী কক্ষ", + "persistentRoomHint": "মিটিং শেষ হওয়ার পরেও স্থায়ী রুমগুলো তালিকাভুক্ত থাকে এবং পুনরায় ব্যবহার করা যায়।", + "noRooms": "এখনো কোনো রুম নেই। মিটিং শুরু করতে একটি রুম তৈরি করুন।", + "openRoom": "খোলা", + "endRoom": "সভা শেষ করুন", + "leaveRoom": "ছেড়ে দিন", + "deleteMember": "অপসারণ করুন", + "removeMember": "{{name}} মুছে ফেলুন", + "removeMemberTitle": "এই সদস্যকে অপসারণ করবেন?", + "removeMemberDescription": "{{name}} অবিলম্বে এই রুমে প্রবেশাধিকার হারাবে। যেকোনো সক্রিয় SSH ভিউয়ার সংযোগ বিচ্ছিন্ন হয়ে যাবে।", + "invite": "আমন্ত্রণ", + "inviteTitle": "সদস্যদের আমন্ত্রণ জানান", + "members": "সদস্যরা", + "membersPanel": "রুমের সদস্য এবং নিয়ন্ত্রণ অনুরোধ", + "membersWithCount": "সদস্যগণ ({{count}})", + "online": "অনলাইন", + "offline": "অফলাইন", + "you": "(তুমি)", + "hostBadge": "হোস্ট", + "presenterBadge": "উপস্থাপন করা হচ্ছে", + "present": "বর্তমান", + "presentTitle": "মঞ্চে উঠুন", + "presentHost": "হোস্ট", + "searchHosts": "হোস্ট অনুসন্ধান করুন", + "noHostsFound": "কোনো উপলব্ধ হোস্ট খুঁজে পাওয়া যায়নি।", + "presentProtocol": "প্রোটোকল", + "stopPresenting": "উপস্থাপন করা বন্ধ করুন", + "takeOver": "দায়িত্ব গ্রহণ করুন", + "takeOverConfirmTitle": "বর্তমান উপস্থাপনাটি প্রতিস্থাপন করবেন?", + "takeOverDescription": "{{name}} এখন উপস্থাপনা করছে। দায়িত্ব গ্রহণ করলে সবাই সেই মঞ্চ থেকে বিচ্ছিন্ন হয়ে যাবে।", + "emptyStage": "কেউ উপস্থাপনা করছেন না। একটি সেশন পরিচালনা করতে মঞ্চে আসুন।", + "stageLoading": "মঞ্চের সাথে সংযোগ স্থাপন...", + "stageEnded": "উপস্থাপনা শেষ হলো।", + "roomEnded": "এই সভা শেষ হয়েছে।", + "presenterLabel": "{{name}} উপস্থাপন করছে", + "guestLink": "অতিথি লিঙ্ক", + "guestLinkOn": "গেস্ট লিঙ্ক চালু আছে। লিঙ্কটি ব্যবহার করে যে কেউ মঞ্চটি দেখতে পারবেন।", + "guestLinkOff": "গেস্ট লিঙ্ক বন্ধ আছে।", + "copyLink": "লিঙ্ক কপি করুন", + "rotateLink": "লিঙ্ক ঘোরান", + "disableLink": "লিঙ্ক নিষ্ক্রিয় করুন", + "rotateLinkConfirmTitle": "গেস্ট লিঙ্কটি ঘোরান?", + "rotateLinkDescription": "বর্তমান সংযোগটি অবিলম্বে কাজ করা বন্ধ করে দেবে। সংযুক্ত SSH গেস্টদের সংযোগ বিচ্ছিন্ন করা হবে।", + "disableLinkConfirmTitle": "গেস্ট লিঙ্কটি নিষ্ক্রিয় করবেন?", + "disableLinkDescription": "বেনামী SSH দর্শকদের সংযোগ অবিলম্বে বিচ্ছিন্ন করা হবে এবং বর্তমান লিঙ্কটি কাজ করা বন্ধ করে দেবে।", + "linkCopied": "লিঙ্ক কপি করা হয়েছে", + "roles": "ভূমিকা", + "users": "ব্যবহারকারীরা", + "searchUsers": "ব্যবহারকারীদের অনুসন্ধান করুন", + "endConfirmTitle": "এই সভা শেষ করব?", + "endDescription": "{{name}} সকলের জন্য শেষ হয়ে যাবে এবং পুনরায় খোলা যাবে না।", + "endPersistentDescription": "{{name}} -এ চলমান উপস্থাপনাটি বন্ধ হয়ে যাবে। রুমটি পুনরায় ব্যবহারের জন্য উপলব্ধ থাকবে।", + "deleteRoom": "রুম মুছুন", + "deleteConfirmTitle": "এই রুমটা মুছে ফেলবেন?", + "deleteDescription": "{{name}} সকলের জন্য স্থায়ীভাবে মুছে ফেলা হবে এবং এটি আর পূর্বাবস্থায় ফেরানো যাবে না।", + "invitedTo": "তোমাকে \"{{name}} \" এ আমন্ত্রণ জানানো হয়েছিল", + "guest": { + "title": "সভা", + "waiting": "উপস্থাপকের জন্য অপেক্ষা করছি...", + "linkInvalid": "এই অতিথি লিঙ্কটি অবৈধ অথবা মিটিং শেষ হয়ে গেছে।" + }, + "reopenFromPanel": "এই মিটিং ট্যাবটির মেয়াদ শেষ হয়ে গেছে। মিটিং প্যানেল থেকে রুমটি পুনরায় খুলুন।", + "requestControl": "অনুরোধ নিয়ন্ত্রণ", + "cancelControlRequest": "অনুরোধ বাতিল করুন", + "controlQueue": "নিয়ন্ত্রণ অনুরোধ", + "noControlRequests": "কোনো অপেক্ষমান অনুরোধ নেই।", + "grantControlTo": "{{name}}-কে নিয়ন্ত্রণ দিন", + "dismissControlRequest": "{{name}}এর নিয়ন্ত্রণ অনুরোধ খারিজ করুন", + "releaseControl": "রিলিজ নিয়ন্ত্রণ", + "grantControl": "নিয়ন্ত্রণ দিন", + "revokeControl": "নিয়ন্ত্রণ ফিরিয়ে নিন", + "controlRequestedBy": "{{name}} নিয়ন্ত্রণের জন্য অনুরোধ করেছে", + "grant": "অনুদান", + "controlBadge": "নিয়ন্ত্রণে", + "youArePresenting": "আপনি অন্য একটি ট্যাবে প্রেজেন্টেশন দিচ্ছেন। মঞ্চ হস্তান্তর করার জন্য প্রেজেন্টেশন বন্ধ করুন।", + "created": "রুম তৈরি করা হয়েছে", + "invited": "সদস্যদের আমন্ত্রণ জানানো হয়েছে", + "roomTab": "সভা" + }, + "nav": { + "collab": "সভা" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "সেশন শেয়ারিং অনুমোদন করুন", + "description": "লাইভ টার্মিনাল, RDP, VNC, এবং টেলনেট সেশনগুলোকে ইনস্ট্যান্স-ব্যাপী শেয়ার করার অনুমতি দিন। এটি নিষ্ক্রিয় করা হলে, প্রতি-হোস্ট শেয়ারিং টগলকে ওভাররাইড করে।" + } + }, + "host": { + "allowSessionSharing": { + "label": "সেশন শেয়ারিং অনুমোদন করুন", + "description": "এই হোস্টের লাইভ সেশনগুলো লিঙ্কের মাধ্যমে বা অন্যান্য ব্যবহারকারীদের সাথে শেয়ার করা যাবে।" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/ca_ES.json b/plugins/session-sharing/locales/translated/ca_ES.json new file mode 100644 index 000000000..ba27dd651 --- /dev/null +++ b/plugins/session-sharing/locales/translated/ca_ES.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Connectant a la sessió compartida...", + "linkInvalid": "Aquest enllaç per compartir no és vàlid, ha caducat o s'ha revocat.", + "rateLimited": "Massa intents, si us plau, torneu-ho a intentar en breu", + "sessionEnded": "Aquesta sessió ha finalitzat", + "readOnlyBadge": "Només visualització", + "ownerLabel": "Propietari de l'amfitrió" + }, + "modalTitle": "Comparteix la sessió", + "shareButton": "Comparteix", + "notReadyToShare": "La sessió encara no està a punt per compartir", + "modeTab": { + "link": "Enllaç", + "user": "Usuari" + }, + "linkModeDescription": "Qualsevol persona amb aquest enllaç s'hi pot unir, no cal tenir cap compte.", + "userModeDescription": "Comparteix amb un usuari específic que ja tingui accés a aquest amfitrió. Si encara no hi té accés, comparteix primer l'amfitrió amb ell o utilitza un enllaç. Un cop compartida, la sessió apareix a la pestanya Connexions.", + "permissionLevel": { + "label": "Nivell de permís", + "readOnly": "Només lectura", + "readOnlyDescription": "Puc veure la sessió en directe però no puc escriure ni interactuar.", + "readWrite": "Lectura i escriptura", + "readWriteDescription": "Pot escriure i interactuar amb la sessió igual que el propietari." + }, + "expiryLabel": "Caducitat de l'enllaç", + "createLinkButton": "Crea un enllaç", + "createShareButton": "Comparteix amb l'usuari", + "searchUsersPlaceholder": "Cerca usuaris...", + "noUsersFound": "No s'han trobat usuaris", + "linkCreated": "Enllaç de compartició creat", + "linkCopied": "Enllaç copiat al porta-retalls", + "copyLink": "Copia l'enllaç", + "shareCreated": "Sessió compartida. Apareixerà a la pestanya Connexions.", + "shareFailed": "No s'ha pogut crear la compartició", + "userLacksHostAccess": "Aquest usuari encara no té accés a aquest amfitrió. Primer comparteix l'amfitrió amb ell o utilitza un enllaç.", + "activeShares": "Accions actives", + "noActiveShares": "No hi ha cap compartició activa per a aquesta sessió", + "revoke": "Revocar", + "revokeConfirmTitle": "Voleu revocar aquesta compartició?", + "revokeConfirmDescription": "Qualsevol persona que utilitzi aquesta opció compartida perdrà l'accés immediatament.", + "revoked": "Compartir revocat", + "revokeFailed": "No s'ha pogut revocar la compartició", + "joinCount": "{{count}} uneix-te", + "joinCount_other": "{{count}} s'uneix", + "expiresAt": "Caduca {{date}}", + "linkShareBadge": "Enllaç", + "userShareBadge": "Usuari: {{username}}", + "loadSharesFailed": "No s'han pogut carregar les comparticions actives", + "customHoursPlaceholder": "Hores fins que caduca l'accés" + }, + "collab": { + "createRoom": "Nova habitació", + "roomName": "Nom de l'habitació", + "persistentRoom": "Sala persistent", + "persistentRoomHint": "Les sales persistents romanen a la llista després que finalitzi la reunió i es poden reutilitzar.", + "noRooms": "Encara no hi ha cap sala. Crea'n una per iniciar una reunió.", + "openRoom": "Obre", + "endRoom": "Finalitza la reunió", + "leaveRoom": "Marxar", + "deleteMember": "Elimina", + "removeMember": "Elimina {{name}}", + "removeMemberTitle": "Voleu eliminar aquest membre?", + "removeMemberDescription": "{{name}} perdrà l'accés a aquesta sala immediatament. Qualsevol visor SSH actiu es desconnectarà.", + "invite": "Convidar", + "inviteTitle": "Convida membres", + "members": "Membres", + "membersPanel": "Membres de la sala i sol·licituds de control", + "membersWithCount": "Membres ({{count}})", + "online": "en línia", + "offline": "fora de línia", + "you": "(tu)", + "hostBadge": "Amfitrió", + "presenterBadge": "Presentant", + "present": "Present", + "presentTitle": "Puja a l'escenari", + "presentHost": "Amfitrió", + "searchHosts": "Cerca amfitrions", + "noHostsFound": "No s'han trobat amfitrions disponibles.", + "presentProtocol": "Protocol", + "stopPresenting": "Atura la presentació", + "takeOver": "Prendre el control", + "takeOverConfirmTitle": "Voleu substituir la presentació actual?", + "takeOverDescription": "{{name}} està presentant ara. Prendre el control desconnectarà tothom d'aquesta etapa.", + "emptyStage": "Ningú fa una presentació. Puja a l'escenari per compartir una sessió.", + "stageLoading": "Connexió amb l'escenari...", + "stageEnded": "La presentació va acabar", + "roomEnded": "Aquesta reunió ha acabat", + "presenterLabel": "{{name}} presenta", + "guestLink": "Enllaç de convidat", + "guestLinkOn": "L'enllaç de convidat està activat. Qualsevol persona que tingui l'enllaç pot veure l'escenari.", + "guestLinkOff": "L'enllaç de convidat està desactivat.", + "copyLink": "Copia l'enllaç", + "rotateLink": "Gira l'enllaç", + "disableLink": "Desactiva l'enllaç", + "rotateLinkConfirmTitle": "Voleu girar l'enllaç de convidat?", + "rotateLinkDescription": "L'enllaç actual deixarà de funcionar immediatament. Els convidats SSH connectats es desconnectaran.", + "disableLinkConfirmTitle": "Voleu desactivar l'enllaç de convidat?", + "disableLinkDescription": "Els visors SSH anònims es desconnectaran immediatament i l'enllaç actual deixarà de funcionar.", + "linkCopied": "Enllaç copiat", + "roles": "Rols", + "users": "Usuaris", + "searchUsers": "Cerca usuaris", + "endConfirmTitle": "Voleu finalitzar aquesta reunió?", + "endDescription": "{{name}} finalitzarà per a tothom i no es podrà tornar a obrir.", + "endPersistentDescription": "La presentació activa a {{name}} s'aturarà. La sala romandrà disponible per a reutilització.", + "deleteRoom": "Suprimeix l'habitació", + "deleteConfirmTitle": "Voleu suprimir aquesta sala?", + "deleteDescription": "{{name}} s'eliminarà permanentment per a tothom i no es podrà desfer.", + "invitedTo": "Has estat convidat a \"{{name}}\"", + "guest": { + "title": "Reunió", + "waiting": "Esperant el presentador...", + "linkInvalid": "Aquest enllaç de convidat no és vàlid o la reunió ha finalitzat." + }, + "reopenFromPanel": "Aquesta pestanya de reunió ha caducat. Torna a obrir la sala des del tauler Reunions.", + "requestControl": "Sol·licitud de control", + "cancelControlRequest": "Cancel·la la sol·licitud", + "controlQueue": "Sol·licituds de control", + "noControlRequests": "No hi ha sol·licituds pendents.", + "grantControlTo": "Dóna el control a {{name}}", + "dismissControlRequest": "Ignora la sol·licitud de control de {{name}}", + "releaseControl": "Control de llançament", + "grantControl": "Donar control", + "revokeControl": "Recupera el control", + "controlRequestedBy": "{{name}} va demanar control", + "grant": "Subvenció", + "controlBadge": "En control", + "youArePresenting": "Esteu presentant en una altra pestanya. Deixeu de presentar per cedir l'escenari.", + "created": "Sala creada", + "invited": "Membres convidats", + "roomTab": "Reunió" + }, + "nav": { + "collab": "Reunions" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Permetre la compartició de sessions", + "description": "Permet que les sessions de terminal, RDP, VNC i Telnet en directe es comparteixin a tota la instància. Anul·la tots els activables i desactivables per compartir per host quan estan desactivats." + } + }, + "host": { + "allowSessionSharing": { + "label": "Permetre la compartició de sessions", + "description": "Permet que les sessions en directe d'aquest amfitrió es comparteixin mitjançant un enllaç o amb altres usuaris" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/cs_CZ.json b/plugins/session-sharing/locales/translated/cs_CZ.json new file mode 100644 index 000000000..de3921f28 --- /dev/null +++ b/plugins/session-sharing/locales/translated/cs_CZ.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Připojování ke sdílené relaci...", + "linkInvalid": "Tento odkaz pro sdílení je neplatný, jeho platnost vypršela nebo byl zrušen.", + "rateLimited": "Příliš mnoho pokusů, zkuste to prosím znovu brzy", + "sessionEnded": "Tato relace skončila", + "readOnlyBadge": "Pouze zobrazení", + "ownerLabel": "Majitel hostitele" + }, + "modalTitle": "Sdílení relace", + "shareButton": "Podíl", + "notReadyToShare": "Relace ještě není připravena ke sdílení", + "modeTab": { + "link": "Odkaz", + "user": "Uživatel" + }, + "linkModeDescription": "Připojit se může kdokoli s tímto odkazem, účet není potřeba.", + "userModeDescription": "Sdílejte s konkrétním uživatelem, který již má k tomuto hostiteli přístup. Pokud ještě nemá přístup, nejprve s ním hostitele sdílejte nebo použijte odkaz. Po sdílení se relace zobrazí na jeho kartě Připojení.", + "permissionLevel": { + "label": "Úroveň oprávnění", + "readOnly": "Pouze pro čtení", + "readOnlyDescription": "Může sledovat relaci živě, ale nemůže psát ani interagovat.", + "readWrite": "Čtení a zápis", + "readWriteDescription": "Může psát a interagovat s relací stejně jako její vlastník." + }, + "expiryLabel": "Platnost vypršení platnosti odkazu", + "createLinkButton": "Vytvořit odkaz", + "createShareButton": "Sdílet s uživatelem", + "searchUsersPlaceholder": "Hledat uživatele...", + "noUsersFound": "Nenaleženi žádní uživatelé", + "linkCreated": "Odkaz pro sdílení vytvořen", + "linkCopied": "Odkaz zkopírován do schránky", + "copyLink": "Kopírovat odkaz", + "shareCreated": "Relace sdílena. Zobrazí se na jejich kartě Připojení.", + "shareFailed": "Vytvoření sdílené položky se nezdařilo", + "userLacksHostAccess": "Tento uživatel zatím nemá přístup k tomuto hostiteli. Nejprve s ním hostitele sdílejte nebo použijte odkaz.", + "activeShares": "Aktivní akcie", + "noActiveShares": "Pro tuto relaci nejsou žádné aktivní sdílení", + "revoke": "Zrušit", + "revokeConfirmTitle": "Zrušit toto sdílení?", + "revokeConfirmDescription": "Každý, kdo tuto sdílenou složku používá, okamžitě ztratí přístup.", + "revoked": "Podíl zrušen", + "revokeFailed": "Zrušení sdílení se nezdařilo", + "joinCount": "{{count}} připojit se", + "joinCount_other": "{{count}} se připojí", + "expiresAt": "Platnost vyprší {{date}}", + "linkShareBadge": "Odkaz", + "userShareBadge": "Uživatel: {{username}}", + "loadSharesFailed": "Nepodařilo se načíst aktivní sdílení", + "customHoursPlaceholder": "Hodiny do vypršení přístupu" + }, + "collab": { + "createRoom": "Nový pokoj", + "roomName": "Název místnosti", + "persistentRoom": "Trvalá místnost", + "persistentRoomHint": "Trvalé místnosti zůstanou uvedeny i po skončení schůzky a lze je znovu použít.", + "noRooms": "Zatím žádné místnosti. Vytvořte si novou a začněte schůzku.", + "openRoom": "OTEVŘENO", + "endRoom": "Ukončit schůzku", + "leaveRoom": "Dovolená", + "deleteMember": "Odstranit", + "removeMember": "Odebrat {{name}}", + "removeMemberTitle": "Odebrat tohoto člena?", + "removeMemberDescription": "{{name}} okamžitě ztratí přístup k této místnosti. Jakýkoli aktivní SSH prohlížeč bude odpojen.", + "invite": "Pozvat", + "inviteTitle": "Pozvat členy", + "members": "Členové", + "membersPanel": "Členové místnosti a požadavky na ovládání", + "membersWithCount": "Členové ({{count}})", + "online": "online", + "offline": "offline", + "you": "(Vy)", + "hostBadge": "Hostitel", + "presenterBadge": "Prezentace", + "present": "Současnost", + "presentTitle": "Vyjdi na pódium", + "presentHost": "Hostitel", + "searchHosts": "Vyhledávání hostitelů", + "noHostsFound": "Nebyli nalezeni žádní dostupní hostitelé.", + "presentProtocol": "Protokol", + "stopPresenting": "Zastavit prezentaci", + "takeOver": "Převzít", + "takeOverConfirmTitle": "Nahradit aktuální prezentaci?", + "takeOverDescription": "{{name}} právě vystupuje. Převzetí vystoupení odpojí všechny od dané scény.", + "emptyStage": "Nikdo neprezentuje. Vydejte se na pódium a sdílejte prezentaci.", + "stageLoading": "Připojení k pódiu...", + "stageEnded": "Prezentace skončila", + "roomEnded": "Tato schůzka skončila", + "presenterLabel": "{{name}} prezentuje", + "guestLink": "Odkaz pro hosty", + "guestLinkOn": "Odkaz pro hosty je zapnutý. Kdokoli s odkazem může sledovat pódium.", + "guestLinkOff": "Hostovský odkaz je vypnutý.", + "copyLink": "Kopírovat odkaz", + "rotateLink": "Otočit odkaz", + "disableLink": "Zakázat odkaz", + "rotateLinkConfirmTitle": "Otočit odkaz pro hosty?", + "rotateLinkDescription": "Aktuální odkaz okamžitě přestane fungovat. Připojení hosté přes SSH budou odpojeni.", + "disableLinkConfirmTitle": "Zakázat odkaz pro hosty?", + "disableLinkDescription": "Anonymní uživatelé SSH budou okamžitě odpojeni a aktuální odkaz přestane fungovat.", + "linkCopied": "Odkaz zkopírován", + "roles": "Role", + "users": "Uživatelé", + "searchUsers": "Vyhledat uživatele", + "endConfirmTitle": "Ukončit tuto schůzku?", + "endDescription": "{{name}} bude pro všechny ukončeno a nebude možné jej znovu otevřít.", + "endPersistentDescription": "Aktivní prezentace v {{name}} se zastaví. Místnost zůstane k dispozici pro opětovné použití.", + "deleteRoom": "Smazat místnost", + "deleteConfirmTitle": "Smazat tuto místnost?", + "deleteDescription": "{{name}} bude trvale smazáno pro všechny a nelze to vrátit zpět.", + "invitedTo": "Byli jste pozváni na „{{name}}“", + "guest": { + "title": "Zasedání", + "waiting": "Čekání na moderátora...", + "linkInvalid": "Tento odkaz pro hosty je neplatný nebo schůzka skončila." + }, + "reopenFromPanel": "Platnost této karty schůzky vypršela. Znovu otevřete místnost z panelu Schůzky.", + "requestControl": "Požádat o kontrolu", + "cancelControlRequest": "Zrušit požadavek", + "controlQueue": "Žádosti o kontrolu", + "noControlRequests": "Žádné čekající žádosti.", + "grantControlTo": "Předejte kontrolu uživateli {{name}}", + "dismissControlRequest": "Zamítnout požadavek uživatele {{name}}na kontrolu", + "releaseControl": "Ovládání uvolnění", + "grantControl": "Předejte kontrolu", + "revokeControl": "Převezměte zpět kontrolu", + "controlRequestedBy": "{{name}} požádal o kontrolu", + "grant": "Grant", + "controlBadge": "Pod kontrolou", + "youArePresenting": "Prezentujete na jiné kartě. Ukončete prezentaci, abyste mohli předat scénu.", + "created": "Místnost vytvořena", + "invited": "Pozvaní členové", + "roomTab": "Zasedání" + }, + "nav": { + "collab": "Schůzky" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Povolit sdílení relace", + "description": "Povolit sdílení relací živého terminálu, RDP, VNC a Telnet v rámci celé instance. Pokud je tato možnost zakázána, přepíše všechny přepínače sdílení pro jednotlivé hostitele." + } + }, + "host": { + "allowSessionSharing": { + "label": "Povolit sdílení relace", + "description": "Umožnit sdílení živých relací na tomto hostiteli prostřednictvím odkazu nebo s ostatními uživateli" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/da_DK.json b/plugins/session-sharing/locales/translated/da_DK.json new file mode 100644 index 000000000..44027251a --- /dev/null +++ b/plugins/session-sharing/locales/translated/da_DK.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Forbinder til delt session...", + "linkInvalid": "Dette delingslink er ugyldigt, udløbet eller er blevet tilbagekaldt", + "rateLimited": "For mange forsøg, prøv igen om lidt", + "sessionEnded": "Denne session er afsluttet", + "readOnlyBadge": "Kun visning", + "ownerLabel": "Værtsejer" + }, + "modalTitle": "Del session", + "shareButton": "Dele", + "notReadyToShare": "Sessionen er ikke klar til deling endnu", + "modeTab": { + "link": "Forbindelse", + "user": "Bruger" + }, + "linkModeDescription": "Alle med dette link kan tilmelde sig, ingen konto kræves.", + "userModeDescription": "Del med en bestemt bruger, der allerede har adgang til denne vært. Hvis de ikke har adgang endnu, skal du først dele værten med dem eller bruge et link i stedet. Når sessionen er delt, vises den under deres fane Forbindelser.", + "permissionLevel": { + "label": "Tilladelsesniveau", + "readOnly": "Skrivebeskyttet", + "readOnlyDescription": "Kan se sessionen live, men kan ikke skrive eller interagere.", + "readWrite": "Læs-skriv", + "readWriteDescription": "Kan skrive og interagere med sessionen ligesom ejeren." + }, + "expiryLabel": "Linkudløb", + "createLinkButton": "Opret link", + "createShareButton": "Del med brugeren", + "searchUsersPlaceholder": "Søg efter brugere...", + "noUsersFound": "Ingen brugere fundet", + "linkCreated": "Delingslink oprettet", + "linkCopied": "Link kopieret til udklipsholder", + "copyLink": "Kopiér link", + "shareCreated": "Session delt. Den vises i fanen Forbindelser.", + "shareFailed": "Kunne ikke oprette deling", + "userLacksHostAccess": "Den bruger har endnu ikke adgang til denne vært. Del værten med dem først, eller brug et link i stedet.", + "activeShares": "Aktive aktier", + "noActiveShares": "Ingen aktive delinger for denne session", + "revoke": "Tilbagekald", + "revokeConfirmTitle": "Tilbagekald denne deling?", + "revokeConfirmDescription": "Enhver, der bruger denne deling, mister adgangen med det samme.", + "revoked": "Deling tilbagekaldt", + "revokeFailed": "Delingen kunne ikke tilbagekaldes", + "joinCount": "{{count}} tilmeld dig", + "joinCount_other": "{{count}} deltager", + "expiresAt": "Udløber {{date}}", + "linkShareBadge": "Forbindelse", + "userShareBadge": "Bruger: {{username}}", + "loadSharesFailed": "Kunne ikke indlæse aktive delinger", + "customHoursPlaceholder": "Timer indtil adgangen udløber" + }, + "collab": { + "createRoom": "Nyt værelse", + "roomName": "Værelsesnavn", + "persistentRoom": "Permanent rum", + "persistentRoomHint": "Permanente rum forbliver på listen efter mødets afslutning og kan genbruges.", + "noRooms": "Ingen rum endnu. Opret et for at starte et møde.", + "openRoom": "Åben", + "endRoom": "Afslut mødet", + "leaveRoom": "Forlade", + "deleteMember": "Fjerne", + "removeMember": "Fjern {{name}}", + "removeMemberTitle": "Fjern dette medlem?", + "removeMemberDescription": "{{name}} mister adgangen til dette rum med det samme. Enhver aktiv SSH-seer vil blive afbrudt.", + "invite": "Invitere", + "inviteTitle": "Inviter medlemmer", + "members": "Medlemmer", + "membersPanel": "Rummedlemmer og kontrolanmodninger", + "membersWithCount": "Medlemmer ({{count}})", + "online": "online", + "offline": "offline", + "you": "(du)", + "hostBadge": "Vært", + "presenterBadge": "Præsentation", + "present": "Nuværende", + "presentTitle": "Indtag scenen", + "presentHost": "Vært", + "searchHosts": "Søg efter værter", + "noHostsFound": "Ingen tilgængelige værter fundet.", + "presentProtocol": "Protokol", + "stopPresenting": "Stop med at præsentere", + "takeOver": "Overtag", + "takeOverConfirmTitle": "Erstat den nuværende præsentation?", + "takeOverDescription": "{{name}} præsenterer nu. Overtagelse vil afbryde alles forbindelse til den scene.", + "emptyStage": "Ingen præsenterer. Gå på scenen for at dele en session.", + "stageLoading": "Forbinder sig til scenen...", + "stageEnded": "Præsentationen sluttede", + "roomEnded": "Dette møde er afsluttet", + "presenterLabel": "{{name}} præsenterer", + "guestLink": "Gæstelink", + "guestLinkOn": "Gæstelinket er aktiveret. Alle med linket kan se scenen.", + "guestLinkOff": "Gæstelinket er slået fra.", + "copyLink": "Kopiér link", + "rotateLink": "Roter link", + "disableLink": "Deaktiver link", + "rotateLinkConfirmTitle": "Roter gæstelinket?", + "rotateLinkDescription": "Det nuværende link vil stoppe med at virke med det samme. Forbundne SSH-gæster vil blive afbrudt.", + "disableLinkConfirmTitle": "Deaktiver gæstelinket?", + "disableLinkDescription": "Anonyme SSH-seere vil blive afbrudt med det samme, og det nuværende link vil stoppe med at virke.", + "linkCopied": "Linket er kopieret", + "roles": "Roller", + "users": "Brugere", + "searchUsers": "Søg brugere", + "endConfirmTitle": "Afslutte dette møde?", + "endDescription": "{{name}} slutter for alle og kan ikke genåbnes.", + "endPersistentDescription": "Den aktive præsentation i {{name}} stopper. Rummet forbliver tilgængeligt til genbrug.", + "deleteRoom": "Slet rum", + "deleteConfirmTitle": "Slet dette rum?", + "deleteDescription": "{{name}} vil blive slettet permanent for alle og kan ikke fortrydes.", + "invitedTo": "Du blev inviteret til \"{{name}}\"", + "guest": { + "title": "Møde", + "waiting": "Venter på præsentatoren...", + "linkInvalid": "Dette gæstelink er ugyldigt, eller mødet er afsluttet" + }, + "reopenFromPanel": "Denne mødefane er udløbet. Genåbn rummet fra panelet Møder.", + "requestControl": "Anmod om kontrol", + "cancelControlRequest": "Annuller anmodning", + "controlQueue": "Kontrolanmodninger", + "noControlRequests": "Ingen ventende anmodninger.", + "grantControlTo": "Giv kontrol til {{name}}", + "dismissControlRequest": "Afvis kontrolanmodningen fra {{name}}", + "releaseControl": "Slipkontrol", + "grantControl": "Giv kontrol", + "revokeControl": "Tag kontrollen tilbage", + "controlRequestedBy": "{{name}} bad om kontrol", + "grant": "Give", + "controlBadge": "I kontrol", + "youArePresenting": "Du præsenterer i en anden fane. Stop præsentationen for at overdrage scenen.", + "created": "Rum oprettet", + "invited": "Medlemmer inviteret", + "roomTab": "Møde" + }, + "nav": { + "collab": "Møder" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Tillad deling af sessioner", + "description": "Tillad deling af live terminal-, RDP-, VNC- og Telnet-sessioner på tværs af instanser. Tilsidesætter alle delingsindstillinger pr. vært, når de er deaktiveret." + } + }, + "host": { + "allowSessionSharing": { + "label": "Tillad deling af sessioner", + "description": "Tillad live-sessioner på denne vært at blive delt via link eller med andre brugere" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/de_DE.json b/plugins/session-sharing/locales/translated/de_DE.json new file mode 100644 index 000000000..5bda3e920 --- /dev/null +++ b/plugins/session-sharing/locales/translated/de_DE.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Verbindung zur gemeinsamen Sitzung wird hergestellt...", + "linkInvalid": "Dieser Freigabelink ist ungültig, abgelaufen oder wurde widerrufen.", + "rateLimited": "Zu viele Versuche, bitte versuchen Sie es in Kürze erneut.", + "sessionEnded": "Diese Sitzung ist beendet.", + "readOnlyBadge": "Nur Ansicht", + "ownerLabel": "Gastgeber" + }, + "modalTitle": "Sitzung teilen", + "shareButton": "Aktie", + "notReadyToShare": "Die Sitzung ist noch nicht zur Freigabe bereit.", + "modeTab": { + "link": "Link", + "user": "Benutzer" + }, + "linkModeDescription": "Jeder mit diesem Link kann beitreten, ein Konto ist nicht erforderlich.", + "userModeDescription": "Teilen Sie die Sitzung mit einem bestimmten Benutzer, der bereits Zugriff auf diesen Host hat. Falls er noch keinen Zugriff hat, teilen Sie ihm den Host zuerst mit oder verwenden Sie stattdessen einen Link. Nach der Freigabe erscheint die Sitzung im Tab „Verbindungen“ des Benutzers.", + "permissionLevel": { + "label": "Berechtigungsstufe", + "readOnly": "Nur lesbar", + "readOnlyDescription": "Ich kann die Sitzung live verfolgen, aber nicht tippen oder interagieren.", + "readWrite": "Lesen und Schreiben", + "readWriteDescription": "Kann in der Sitzung genauso tippen und mit ihr interagieren wie der Besitzer." + }, + "expiryLabel": "Linkablauf", + "createLinkButton": "Verknüpfung erstellen", + "createShareButton": "Mit dem Nutzer teilen", + "searchUsersPlaceholder": "Benutzer suchen...", + "noUsersFound": "Keine Benutzer gefunden", + "linkCreated": "Link zum Teilen erstellt", + "linkCopied": "Link in die Zwischenablage kopiert", + "copyLink": "Link kopieren", + "shareCreated": "Die Sitzung wurde freigegeben. Sie wird im Tab „Verbindungen“ des Benutzers angezeigt.", + "shareFailed": "Fehler beim Erstellen der Freigabe", + "userLacksHostAccess": "Dieser Benutzer hat noch keinen Zugriff auf diesen Host. Teilen Sie ihm den Host zuerst mit oder verwenden Sie stattdessen einen Link.", + "activeShares": "Aktive Anteile", + "noActiveShares": "Keine aktiven Anteile für diese Sitzung", + "revoke": "Widerrufen", + "revokeConfirmTitle": "Diese Aktie widerrufen?", + "revokeConfirmDescription": "Wer diese Freigabe nutzt, verliert sofort den Zugriff.", + "revoked": "Anteil widerrufen", + "revokeFailed": "Aktienwiderruf fehlgeschlagen", + "joinCount": "{{count}} beitreten", + "joinCount_other": "{{count}} verbindet sich", + "expiresAt": "Läuft ab {{date}}", + "linkShareBadge": "Link", + "userShareBadge": "Benutzer: {{username}}", + "loadSharesFailed": "Fehler beim Laden der aktiven Anteile", + "customHoursPlaceholder": "Stunden bis zum Zugriffsablauf" + }, + "collab": { + "createRoom": "Neues Zimmer", + "roomName": "Zimmername", + "persistentRoom": "Persistenter Raum", + "persistentRoomHint": "Die gespeicherten Räume bleiben auch nach Ende des Meetings gelistet und können wiederverwendet werden.", + "noRooms": "Noch keine Räume vorhanden. Erstellen Sie einen, um ein Meeting zu starten.", + "openRoom": "Offen", + "endRoom": "Ende der Besprechung", + "leaveRoom": "Verlassen", + "deleteMember": "Entfernen", + "removeMember": "Entferne {{name}}", + "removeMemberTitle": "Dieses Mitglied entfernen?", + "removeMemberDescription": "{{name}} verliert sofort den Zugriff auf diesen Raum. Alle aktiven SSH-Viewer werden getrennt.", + "invite": "Einladen", + "inviteTitle": "Mitglieder einladen", + "members": "Mitglieder", + "membersPanel": "Raummitglieder und Kontrollanfragen", + "membersWithCount": "Mitglieder ({{count}})", + "online": "online", + "offline": "offline", + "you": "(Du)", + "hostBadge": "Gastgeber", + "presenterBadge": "Präsentation", + "present": "Gegenwärtig", + "presentTitle": "Betretet die Bühne", + "presentHost": "Gastgeber", + "searchHosts": "Suchmaschinen", + "noHostsFound": "Es wurden keine verfügbaren Hosts gefunden.", + "presentProtocol": "Protokoll", + "stopPresenting": "Hör auf zu präsentieren", + "takeOver": "Übernehmen", + "takeOverConfirmTitle": "Die aktuelle Präsentation ersetzen?", + "takeOverDescription": "{{name}} präsentiert sich jetzt. Die Übernahme trennt alle von dieser Bühne.", + "emptyStage": "Niemand präsentiert. Ergreife die Bühne, um eine Session zu teilen.", + "stageLoading": "Verbindung zur Bühne...", + "stageEnded": "Die Präsentation endete", + "roomEnded": "Diese Sitzung ist beendet.", + "presenterLabel": "{{name}} präsentiert", + "guestLink": "Gastlink", + "guestLinkOn": "Der Gastzugang ist aktiviert. Jeder mit dem Link kann die Bühnenshow verfolgen.", + "guestLinkOff": "Der Gastlink ist deaktiviert.", + "copyLink": "Link kopieren", + "rotateLink": "Drehverbindung", + "disableLink": "Link deaktivieren", + "rotateLinkConfirmTitle": "Den Gastlink drehen?", + "rotateLinkDescription": "Die bestehende Verbindung wird sofort unterbrochen. Verbundene SSH-Gäste werden getrennt.", + "disableLinkConfirmTitle": "Gastlink deaktivieren?", + "disableLinkDescription": "Anonyme SSH-Zugriffe werden sofort getrennt und die aktuelle Verbindung funktioniert nicht mehr.", + "linkCopied": "Link kopiert", + "roles": "Rollen", + "users": "Benutzer", + "searchUsers": "Nutzer suchen", + "endConfirmTitle": "Diese Sitzung beenden?", + "endDescription": "{{name}} wird für alle beendet und kann nicht wieder geöffnet werden.", + "endPersistentDescription": "Die aktive Präsentation in {{name}} wird beendet. Der Raum steht weiterhin zur erneuten Verwendung zur Verfügung.", + "deleteRoom": "Raum löschen", + "deleteConfirmTitle": "Diesen Raum löschen?", + "deleteDescription": "{{name}} wird für alle dauerhaft gelöscht und kann nicht rückgängig gemacht werden.", + "invitedTo": "Sie wurden eingeladen zu \"{{name}}\"", + "guest": { + "title": "Treffen", + "waiting": "Ich warte auf den Moderator...", + "linkInvalid": "Dieser Gastlink ist ungültig oder das Meeting ist beendet." + }, + "reopenFromPanel": "Dieser Besprechungsreiter ist abgelaufen. Öffnen Sie den Raum erneut über das Besprechungsmenü.", + "requestControl": "Anforderungskontrolle", + "cancelControlRequest": "Stornierungsanfrage", + "controlQueue": "Kontrollanfragen", + "noControlRequests": "Keine offenen Anfragen.", + "grantControlTo": "Übergib die Kontrolle an {{name}}", + "dismissControlRequest": "Kontrollanfrage von {{name}}ablehnen", + "releaseControl": "Freigabekontrolle", + "grantControl": "Kontrolle abgeben", + "revokeControl": "Übernehmen Sie wieder die Kontrolle", + "controlRequestedBy": "{{name}} hat um Kontrolle gebeten", + "grant": "Gewähren", + "controlBadge": "Unter Kontrolle", + "youArePresenting": "Sie präsentieren in einem anderen Tab. Beenden Sie Ihre Präsentation, um die Bühne zu übergeben.", + "created": "Raum geschaffen", + "invited": "Mitglieder eingeladen", + "roomTab": "Treffen" + }, + "nav": { + "collab": "Treffen" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Sitzungsfreigabe zulassen", + "description": "Ermöglicht die instanzweite Freigabe von Live-Terminal-, RDP-, VNC- und Telnet-Sitzungen. Überschreibt alle Freigabeeinstellungen pro Host, wenn diese deaktiviert sind." + } + }, + "host": { + "allowSessionSharing": { + "label": "Sitzungsfreigabe zulassen", + "description": "Ermöglichen Sie es, Live-Sitzungen auf diesem Host per Link oder mit anderen Benutzern zu teilen." + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/el_GR.json b/plugins/session-sharing/locales/translated/el_GR.json new file mode 100644 index 000000000..df41288a3 --- /dev/null +++ b/plugins/session-sharing/locales/translated/el_GR.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Σύνδεση σε κοινόχρηστη συνεδρία...", + "linkInvalid": "Αυτός ο σύνδεσμος κοινής χρήσης δεν είναι έγκυρος, έχει λήξει ή έχει ανακληθεί", + "rateLimited": "Πάρα πολλές προσπάθειες, δοκιμάστε ξανά σύντομα.", + "sessionEnded": "Αυτή η συνεδρία έληξε", + "readOnlyBadge": "Μόνο προβολή", + "ownerLabel": "Κάτοχος κεντρικού υπολογιστή" + }, + "modalTitle": "Κοινή χρήση περιόδου σύνδεσης", + "shareButton": "Μερίδιο", + "notReadyToShare": "Η συνεδρία δεν είναι ακόμη έτοιμη για κοινή χρήση", + "modeTab": { + "link": "Σύνδεσμος", + "user": "Μεταχειριζόμενος" + }, + "linkModeDescription": "Οποιοσδήποτε έχει αυτόν τον σύνδεσμο μπορεί να συμμετάσχει, δεν απαιτείται λογαριασμός.", + "userModeDescription": "Κοινοποίηση με έναν συγκεκριμένο χρήστη που έχει ήδη πρόσβαση σε αυτόν τον κεντρικό υπολογιστή. Εάν δεν έχει ακόμη πρόσβαση, μοιραστείτε πρώτα τον κεντρικό υπολογιστή μαζί του ή χρησιμοποιήστε έναν σύνδεσμο. Μόλις κοινοποιηθεί, η συνεδρία εμφανίζεται στην καρτέλα Συνδέσεις.", + "permissionLevel": { + "label": "Επίπεδο δικαιωμάτων", + "readOnly": "Μόνο για ανάγνωση", + "readOnlyDescription": "Μπορώ να παρακολουθήσω την συνεδρία ζωντανά, αλλά δεν μπορώ να πληκτρολογήσω ή να αλληλεπιδράσω.", + "readWrite": "Ανάγνωση-εγγραφή", + "readWriteDescription": "Μπορεί να πληκτρολογήσει και να αλληλεπιδράσει με την περίοδο λειτουργίας όπως ο κάτοχος." + }, + "expiryLabel": "Λήξη σύνδεσης", + "createLinkButton": "Δημιουργία συνδέσμου", + "createShareButton": "Κοινοποίηση με χρήστη", + "searchUsersPlaceholder": "Αναζήτηση χρηστών...", + "noUsersFound": "Δεν βρέθηκαν χρήστες", + "linkCreated": "Ο σύνδεσμος κοινής χρήσης δημιουργήθηκε", + "linkCopied": "Ο σύνδεσμος αντιγράφηκε στο πρόχειρο", + "copyLink": "Αντιγραφή συνδέσμου", + "shareCreated": "Η συνεδρία κοινοποιήθηκε. Θα εμφανιστεί στην καρτέλα \"Συνδέσεις\".", + "shareFailed": "Η δημιουργία κοινής χρήσης απέτυχε", + "userLacksHostAccess": "Αυτός ο χρήστης δεν έχει ακόμη πρόσβαση σε αυτόν τον κεντρικό υπολογιστή. Κοινοποιήστε πρώτα τον κεντρικό υπολογιστή μαζί του ή χρησιμοποιήστε έναν σύνδεσμο.", + "activeShares": "Ενεργές κοινοποιήσεις", + "noActiveShares": "Δεν υπάρχουν ενεργές κοινοποιήσεις για αυτήν την περίοδο σύνδεσης", + "revoke": "Ανακαλώ", + "revokeConfirmTitle": "Ανάκληση αυτής της κοινοποίησης;", + "revokeConfirmDescription": "Όποιος χρησιμοποιεί αυτήν την κοινοποίηση θα χάσει αμέσως την πρόσβαση.", + "revoked": "Η κοινοποίηση ανακλήθηκε", + "revokeFailed": "Η ανάκληση της κοινής χρήσης απέτυχε", + "joinCount": "{{count}} συμμετοχή", + "joinCount_other": "{{count}} ενώσεις", + "expiresAt": "Λήγει {{date}}", + "linkShareBadge": "Σύνδεσμος", + "userShareBadge": "Χρήστης: {{username}}", + "loadSharesFailed": "Αποτυχία φόρτωσης ενεργών κοινόχρηστων στοιχείων", + "customHoursPlaceholder": "Ώρες μέχρι τη λήξη της πρόσβασης" + }, + "collab": { + "createRoom": "Νέο δωμάτιο", + "roomName": "Όνομα δωματίου", + "persistentRoom": "Μόνιμο δωμάτιο", + "persistentRoomHint": "Οι μόνιμες αίθουσες παραμένουν καταχωρημένες μετά το τέλος της σύσκεψης και μπορούν να επαναχρησιμοποιηθούν.", + "noRooms": "Δεν υπάρχουν ακόμη αίθουσες. Δημιουργήστε μία για να ξεκινήσετε μια σύσκεψη.", + "openRoom": "Ανοιχτό", + "endRoom": "Λήξη συνάντησης", + "leaveRoom": "Αδεια", + "deleteMember": "Αφαιρώ", + "removeMember": "Αφαίρεση {{name}}", + "removeMemberTitle": "Κατάργηση αυτού του μέλους;", + "removeMemberDescription": "Ο χρήστης {{name}} θα χάσει αμέσως την πρόσβαση σε αυτό το δωμάτιο. Οποιοσδήποτε ενεργός χρήστης προβολής SSH θα αποσυνδεθεί.", + "invite": "Καλώ", + "inviteTitle": "Πρόσκληση μελών", + "members": "Μέλη", + "membersPanel": "Μέλη δωματίου και αιτήματα ελέγχου", + "membersWithCount": "Μέλη ({{count}})", + "online": "διαδικτυακά", + "offline": "εκτός σύνδεσης", + "you": "(εσείς)", + "hostBadge": "Πλήθος", + "presenterBadge": "Παρουσίαση", + "present": "Παρόν", + "presentTitle": "Ανέβα στη σκηνή", + "presentHost": "Πλήθος", + "searchHosts": "Αναζήτηση κεντρικών υπολογιστών", + "noHostsFound": "Δεν βρέθηκαν διαθέσιμοι κεντρικοί υπολογιστές.", + "presentProtocol": "Πρωτόκολλο", + "stopPresenting": "Διακοπή παρουσίασης", + "takeOver": "Αναλαμβάνω", + "takeOverConfirmTitle": "Αντικατάσταση της τρέχουσας παρουσίασης;", + "takeOverDescription": "Το {{name}} παρουσιάζεται τώρα. Η ανάληψη του ελέγχου θα αποσυνδέσει όλους από αυτό το στάδιο.", + "emptyStage": "Κανείς δεν κάνει παρουσίαση. Ανεβείτε στη σκηνή για να μοιραστείτε μια συνεδρία.", + "stageLoading": "Σύνδεση με τη σκηνή...", + "stageEnded": "Η παρουσίαση έληξε", + "roomEnded": "Αυτή η συνάντηση έληξε", + "presenterLabel": "{{name}} παρουσιάζει", + "guestLink": "Σύνδεσμος επισκέπτη", + "guestLinkOn": "Ο σύνδεσμος επισκεπτών είναι ενεργοποιημένος. Όποιος έχει τον σύνδεσμο μπορεί να παρακολουθήσει τη σκηνή.", + "guestLinkOff": "Ο σύνδεσμος επισκέπτη είναι απενεργοποιημένος.", + "copyLink": "Αντιγραφή συνδέσμου", + "rotateLink": "Περιστροφή συνδέσμου", + "disableLink": "Απενεργοποίηση συνδέσμου", + "rotateLinkConfirmTitle": "Εναλλαγή του συνδέσμου επισκέπτη;", + "rotateLinkDescription": "Ο τρέχων σύνδεσμος θα σταματήσει να λειτουργεί αμέσως. Οι συνδεδεμένοι επισκέπτες SSH θα αποσυνδεθούν.", + "disableLinkConfirmTitle": "Απενεργοποίηση του συνδέσμου επισκέπτη;", + "disableLinkDescription": "Οι ανώνυμοι θεατές SSH θα αποσυνδεθούν αμέσως και ο τρέχων σύνδεσμος θα σταματήσει να λειτουργεί.", + "linkCopied": "Ο σύνδεσμος αντιγράφηκε", + "roles": "Ρόλοι", + "users": "Χρήστες", + "searchUsers": "Αναζήτηση χρηστών", + "endConfirmTitle": "Λήξη αυτής της συνάντησης;", + "endDescription": "Το {{name}} θα λήξει για όλους και δεν μπορεί να ανοίξει ξανά.", + "endPersistentDescription": "Η ενεργή παρουσίαση στο {{name}} θα σταματήσει. Η αίθουσα παραμένει διαθέσιμη για επαναχρησιμοποίηση.", + "deleteRoom": "Διαγραφή δωματίου", + "deleteConfirmTitle": "Διαγραφή αυτού του δωματίου;", + "deleteDescription": "Η εντολή {{name}} θα διαγραφεί οριστικά για όλους και δεν θα είναι δυνατή η αναίρεση της.", + "invitedTo": "Σας προσκάλεσαν στο \"{{name}}\"", + "guest": { + "title": "Συνάντηση", + "waiting": "Περιμένοντας τον παρουσιαστή...", + "linkInvalid": "Αυτός ο σύνδεσμος επισκέπτη δεν είναι έγκυρος ή η σύσκεψη έχει λήξει" + }, + "reopenFromPanel": "Αυτή η καρτέλα σύσκεψης έληξε. Ανοίξτε ξανά την αίθουσα από τον πίνακα \"Συνέδρια\".", + "requestControl": "Αίτημα ελέγχου", + "cancelControlRequest": "Αίτημα ακύρωσης", + "controlQueue": "Αιτήματα ελέγχου", + "noControlRequests": "Δεν υπάρχουν εκκρεμή αιτήματα.", + "grantControlTo": "Δώστε τον έλεγχο σε {{name}}", + "dismissControlRequest": "Απόρριψη αιτήματος ελέγχου του {{name}}", + "releaseControl": "Έλεγχος απελευθέρωσης", + "grantControl": "Δώστε τον έλεγχο", + "revokeControl": "Πάρε πίσω τον έλεγχο", + "controlRequestedBy": "{{name}} ζήτησε έλεγχο", + "grant": "Χορήγηση", + "controlBadge": "Υπό έλεγχο", + "youArePresenting": "Κάνετε παρουσίαση σε άλλη καρτέλα. Διακόψτε την παρουσίαση για να παραδώσετε τη σκηνή.", + "created": "Δημιουργήθηκε δωμάτιο", + "invited": "Μέλη που έχουν προσκληθεί", + "roomTab": "Συνάντηση" + }, + "nav": { + "collab": "Συναντήσεις" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Επιτρέψτε την κοινή χρήση περιόδου σύνδεσης", + "description": "Επιτρέπεται η κοινή χρήση των ζωντανών συνεδριών τερματικού, RDP, VNC και Telnet σε ολόκληρη την παρουσία. Παρακάμπτει κάθε εναλλαγή κοινής χρήσης ανά κεντρικό υπολογιστή όταν είναι απενεργοποιημένη." + } + }, + "host": { + "allowSessionSharing": { + "label": "Επιτρέψτε την κοινή χρήση περιόδου σύνδεσης", + "description": "Να επιτρέπεται η κοινοποίηση ζωντανών συνεδριών σε αυτόν τον πάροχο μέσω συνδέσμου ή με άλλους χρήστες" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/es_ES.json b/plugins/session-sharing/locales/translated/es_ES.json new file mode 100644 index 000000000..d3f05d5f2 --- /dev/null +++ b/plugins/session-sharing/locales/translated/es_ES.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Conectando con la sesión compartida...", + "linkInvalid": "Este enlace para compartir no es válido, ha caducado o ha sido revocado.", + "rateLimited": "Demasiados intentos, por favor inténtelo de nuevo en breve.", + "sessionEnded": "Esta sesión ha finalizado.", + "readOnlyBadge": "Solo lectura", + "ownerLabel": "Propietario del anfitrión" + }, + "modalTitle": "Compartir sesión", + "shareButton": "Compartir", + "notReadyToShare": "La sesión aún no está lista para compartirse.", + "modeTab": { + "link": "Enlace", + "user": "Usuario" + }, + "linkModeDescription": "Cualquier persona con este enlace puede unirse, no se requiere cuenta.", + "userModeDescription": "Comparte la sesión con un usuario específico que ya tenga acceso a este host. Si aún no tiene acceso, comparte primero el host con él o utiliza un enlace. Una vez compartida, la sesión aparecerá en su pestaña Conexiones.", + "permissionLevel": { + "label": "Nivel de permiso", + "readOnly": "Solo lectura", + "readOnlyDescription": "Se puede ver la sesión en directo, pero no se puede escribir ni interactuar.", + "readWrite": "Leer y escribir", + "readWriteDescription": "Puede escribir e interactuar con la sesión igual que el propietario." + }, + "expiryLabel": "Caducidad del enlace", + "createLinkButton": "Crear enlace", + "createShareButton": "Compartir con el usuario", + "searchUsersPlaceholder": "Buscar usuarios...", + "noUsersFound": "No se encontraron usuarios", + "linkCreated": "Enlace para compartir creado", + "linkCopied": "Enlace copiado al portapapeles", + "copyLink": "Copiar enlace", + "shareCreated": "Sesión compartida. Aparecerá en su pestaña Conexiones.", + "shareFailed": "No se pudo crear el recurso compartido", + "userLacksHostAccess": "Ese usuario aún no tiene acceso a este servidor. Primero, comparte el servidor con él o utiliza un enlace.", + "activeShares": "Acciones activas", + "noActiveShares": "No hay acciones activas para esta sesión.", + "revoke": "Revocar", + "revokeConfirmTitle": "¿Revocar esta acción?", + "revokeConfirmDescription": "Cualquier persona que utilice este recurso compartido perderá el acceso inmediatamente.", + "revoked": "Acción revocada", + "revokeFailed": "No se pudo revocar la participación.", + "joinCount": "{{count}} unirse", + "joinCount_other": "{{count}} se une", + "expiresAt": "Caduca {{date}}", + "linkShareBadge": "Enlace", + "userShareBadge": "Usuario: {{username}}", + "loadSharesFailed": "No se pudieron cargar los recursos compartidos activos.", + "customHoursPlaceholder": "Horas hasta que caduque el acceso" + }, + "collab": { + "createRoom": "Habitación nueva", + "roomName": "Nombre de la habitación", + "persistentRoom": "Habitación persistente", + "persistentRoomHint": "Las salas reservadas permanecen en la lista una vez finalizada la reunión y pueden volver a utilizarse.", + "noRooms": "Aún no hay salas disponibles. Crea una para iniciar una reunión.", + "openRoom": "Abierto", + "endRoom": "Fin de la reunión", + "leaveRoom": "Dejar", + "deleteMember": "Eliminar", + "removeMember": "Eliminar {{name}}", + "removeMemberTitle": "¿Eliminar a este miembro?", + "removeMemberDescription": "{{name}} perderá el acceso a esta sala inmediatamente. Cualquier visor SSH activo será desconectado.", + "invite": "Invitar", + "inviteTitle": "Invitar a los miembros", + "members": "Miembros", + "membersPanel": "Miembros de la sala y solicitudes de control", + "membersWithCount": "Miembros ({{count}})", + "online": "en línea", + "offline": "desconectado", + "you": "(tú)", + "hostBadge": "Anfitrión", + "presenterBadge": "Presentación", + "present": "Presente", + "presentTitle": "Sube al escenario", + "presentHost": "Anfitrión", + "searchHosts": "Buscar hosts", + "noHostsFound": "No se encontraron hosts disponibles.", + "presentProtocol": "Protocolo", + "stopPresenting": "Deja de presentar", + "takeOver": "Tomar el control", + "takeOverConfirmTitle": "¿Reemplazar la presentación actual?", + "takeOverDescription": "{{name}} se está presentando ahora. Tomar el control desconectará a todos de ese escenario.", + "emptyStage": "Nadie va a presentar. Sube al escenario para compartir una sesión.", + "stageLoading": "Conectando con el escenario...", + "stageEnded": "La presentación terminó", + "roomEnded": "Esta reunión ha terminado.", + "presenterLabel": "{{name}} está presentando", + "guestLink": "Enlace de invitado", + "guestLinkOn": "El enlace para invitados está activado. Cualquiera que tenga el enlace puede ver el espectáculo.", + "guestLinkOff": "El enlace de invitado está desactivado.", + "copyLink": "Copiar enlace", + "rotateLink": "Enlace giratorio", + "disableLink": "Deshabilitar enlace", + "rotateLinkConfirmTitle": "¿Rotar el enlace de invitado?", + "rotateLinkDescription": "El enlace actual dejará de funcionar inmediatamente. Los invitados SSH conectados serán desconectados.", + "disableLinkConfirmTitle": "¿Deshabilitar el enlace de invitado?", + "disableLinkDescription": "Los usuarios que intenten acceder de forma anónima mediante SSH serán desconectados inmediatamente y el enlace actual dejará de funcionar.", + "linkCopied": "Enlace copiado", + "roles": "Roles", + "users": "Usuarios", + "searchUsers": "Buscar usuarios", + "endConfirmTitle": "¿Dar por terminada esta reunión?", + "endDescription": "{{name}} finalizará para todos y no podrá reabrirse.", + "endPersistentDescription": "La presentación activa en {{name}} se detendrá. La sala permanece disponible para su reutilización.", + "deleteRoom": "Eliminar habitación", + "deleteConfirmTitle": "¿Eliminar esta habitación?", + "deleteDescription": "{{name}} se eliminará permanentemente para todos y no se podrá deshacer.", + "invitedTo": "Te invitaron a \"{{name}}\"", + "guest": { + "title": "Reunión", + "waiting": "Esperando al presentador...", + "linkInvalid": "Este enlace de invitado no es válido o la reunión ha finalizado." + }, + "reopenFromPanel": "Esta pestaña de reunión ha caducado. Vuelva a abrir la sala desde el panel de Reuniones.", + "requestControl": "Control de solicitudes", + "cancelControlRequest": "Cancelar solicitud", + "controlQueue": "Solicitudes de control", + "noControlRequests": "No hay solicitudes pendientes.", + "grantControlTo": "Cede el control a {{name}}", + "dismissControlRequest": "Rechazar la solicitud de control de {{name}}", + "releaseControl": "Control de liberación", + "grantControl": "Cede el control", + "revokeControl": "Recupera el control", + "controlRequestedBy": "{{name}} pidió control", + "grant": "Conceder", + "controlBadge": "En control", + "youArePresenting": "Estás presentando en otra pestaña. Deja de presentar para ceder la palabra.", + "created": "Habitación creada", + "invited": "Miembros invitados", + "roomTab": "Reunión" + }, + "nav": { + "collab": "Reuniones" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Permitir compartir la sesión", + "description": "Permite compartir sesiones de terminal en vivo, RDP, VNC y Telnet en toda la instancia. Cuando está desactivado, anula cualquier configuración de uso compartido por host." + } + }, + "host": { + "allowSessionSharing": { + "label": "Permitir compartir la sesión", + "description": "Permitir que las sesiones en vivo en este servidor se compartan mediante un enlace o con otros usuarios." + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/fi_FI.json b/plugins/session-sharing/locales/translated/fi_FI.json new file mode 100644 index 000000000..a841deecf --- /dev/null +++ b/plugins/session-sharing/locales/translated/fi_FI.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Yhdistetään jaettuun istuntoon...", + "linkInvalid": "Tämä jakolinkki on virheellinen, vanhentunut tai se on peruutettu.", + "rateLimited": "Liikaa yrityksiä, yritä uudelleen pian", + "sessionEnded": "Tämä istunto on päättynyt", + "readOnlyBadge": "Vain katselu", + "ownerLabel": "Isännän omistaja" + }, + "modalTitle": "Jaa istunto", + "shareButton": "Jakaa", + "notReadyToShare": "Istunto ei ole vielä valmis jaettavaksi", + "modeTab": { + "link": "Linkki", + "user": "Käyttäjä" + }, + "linkModeDescription": "Kuka tahansa, jolla on tämä linkki, voi liittyä, tiliä ei tarvita.", + "userModeDescription": "Jaa tietyn käyttäjän kanssa, jolla on jo pääsy tähän isäntään. Jos heillä ei vielä ole pääsyä, jaa isäntä ensin heidän kanssaan tai käytä linkkiä. Jakamisen jälkeen istunto näkyy heidän Yhteydet-välilehdellään.", + "permissionLevel": { + "label": "Käyttöoikeustaso", + "readOnly": "Vain lukuoikeus", + "readOnlyDescription": "Voit katsoa istuntoa suorana, mutta et voi kirjoittaa tai olla vuorovaikutuksessa.", + "readWrite": "Lue ja kirjoita", + "readWriteDescription": "Voi kirjoittaa ja olla vuorovaikutuksessa istunnon kanssa aivan kuten omistaja." + }, + "expiryLabel": "Linkin vanheneminen", + "createLinkButton": "Luo linkki", + "createShareButton": "Jaa käyttäjän kanssa", + "searchUsersPlaceholder": "Hae käyttäjiä...", + "noUsersFound": "Ei käyttäjiä löytynyt", + "linkCreated": "Jaa linkki luotu", + "linkCopied": "Linkki kopioitu leikepöydälle", + "copyLink": "Kopioi linkki", + "shareCreated": "Istunto jaettu. Se näkyy heidän Yhteydet-välilehdellä.", + "shareFailed": "Jaon luominen epäonnistui", + "userLacksHostAccess": "Käyttäjällä ei ole vielä pääsyä tähän isäntään. Jaa isäntä ensin heidän kanssaan tai käytä linkkiä.", + "activeShares": "Aktiiviset jaot", + "noActiveShares": "Ei aktiivisia jakoja tällä istunnolla", + "revoke": "Peruuttaa", + "revokeConfirmTitle": "Perutaanko tämä jako?", + "revokeConfirmDescription": "Kuka tahansa tätä jakoa käyttävä menettää käyttöoikeuden välittömästi.", + "revoked": "Jako peruttu", + "revokeFailed": "Jakamisen peruuttaminen epäonnistui", + "joinCount": "{{count}} liity", + "joinCount_other": "{{count}} liitokset", + "expiresAt": "Vanhenee {{date}}", + "linkShareBadge": "Linkki", + "userShareBadge": "Käyttäjä: {{username}}", + "loadSharesFailed": "Aktiivisten jakojen lataaminen epäonnistui", + "customHoursPlaceholder": "Tuntia käyttöoikeuden vanhenemiseen" + }, + "collab": { + "createRoom": "Uusi huone", + "roomName": "Huoneen nimi", + "persistentRoom": "Pysyvä huone", + "persistentRoomHint": "Pysyvät huoneet pysyvät luettelossa kokouksen päättymisen jälkeen ja niitä voidaan käyttää uudelleen.", + "noRooms": "Ei vielä huoneita. Luo huone aloittaaksesi kokouksen.", + "openRoom": "Avata", + "endRoom": "Kokouksen loppu", + "leaveRoom": "Lähtö", + "deleteMember": "Poistaa", + "removeMember": "Poista {{name}}", + "removeMemberTitle": "Poistetaanko tämä jäsen?", + "removeMemberDescription": "{{name}} menettää pääsyn tähän huoneeseen välittömästi. Kaikki aktiiviset SSH-katseluohjelmat katkaistaan.", + "invite": "Kutsu", + "inviteTitle": "Kutsu jäseniä", + "members": "Jäsenet", + "membersPanel": "Huoneen jäsenet ja hallintapyynnöt", + "membersWithCount": "Jäsenet ({{count}})", + "online": "verkossa", + "offline": "offline-tilassa", + "you": "(sinä)", + "hostBadge": "Isäntä", + "presenterBadge": "Esittäminen", + "present": "Esittää", + "presentTitle": "Nouse lavalle", + "presentHost": "Isäntä", + "searchHosts": "Hae isäntiä", + "noHostsFound": "Ei saatavilla olevia isäntiä.", + "presentProtocol": "Protokolla", + "stopPresenting": "Lopeta esittäminen", + "takeOver": "Ota haltuunsa", + "takeOverConfirmTitle": "Korvataanko nykyinen esitys?", + "takeOverDescription": "{{name}} esittää nyt. Hallinnan ottaminen irrottaa kaikki yhteydet kyseiseen vaiheeseen.", + "emptyStage": "Kukaan ei esitä. Nouse lavalle ja jaa sessio.", + "stageLoading": "Yhdistetään lavalle...", + "stageEnded": "Esitys päättyi", + "roomEnded": "Tämä kokous on päättynyt", + "presenterLabel": "{{name}} esittää", + "guestLink": "Vieraslinkki", + "guestLinkOn": "Vieraslinkki on käytössä. Kuka tahansa linkin omaava voi katsoa lavaa.", + "guestLinkOff": "Vierailijalinkki on pois päältä.", + "copyLink": "Kopioi linkki", + "rotateLink": "Kierrä linkkiä", + "disableLink": "Poista linkki käytöstä", + "rotateLinkConfirmTitle": "Kierretäänkö vieraslinkkiä?", + "rotateLinkDescription": "Nykyinen linkki lakkaa toimimasta välittömästi. Yhdistettyjen SSH-vieraiden yhteys katkaistaan.", + "disableLinkConfirmTitle": "Poistetaanko vieraslinkki käytöstä?", + "disableLinkDescription": "Anonyymien SSH-katsojien yhteys katkaistaan välittömästi ja nykyinen linkki lakkaa toimimasta.", + "linkCopied": "Linkki kopioitu", + "roles": "Roolit", + "users": "Käyttäjät", + "searchUsers": "Hae käyttäjiä", + "endConfirmTitle": "Lopetetaanko tämä kokous?", + "endDescription": "{{name}} päättyy kaikilta, eikä sitä voida avata uudelleen.", + "endPersistentDescription": "Aktiivinen esitys kohdassa {{name}} pysähtyy. Huone pysyy käytettävissä uudelleen.", + "deleteRoom": "Poista huone", + "deleteConfirmTitle": "Poistetaanko tämä huone?", + "deleteDescription": "{{name}} poistetaan pysyvästi kaikilta, eikä sitä voi perua.", + "invitedTo": "Sinut kutsuttiin \"{{name}} \" -palveluun", + "guest": { + "title": "Kokous", + "waiting": "Esiintyjää odotellessa...", + "linkInvalid": "Tämä vieraslinkki on virheellinen tai kokous on päättynyt" + }, + "reopenFromPanel": "Tämä kokousvälilehti vanheni. Avaa huone uudelleen Kokoukset-paneelista.", + "requestControl": "Pyydä hallintaa", + "cancelControlRequest": "Peruuta pyyntö", + "controlQueue": "Ohjauspyynnöt", + "noControlRequests": "Ei vireillä olevia pyyntöjä.", + "grantControlTo": "Anna hallinta käyttäjälle {{name}}", + "dismissControlRequest": "Hylkää käyttäjän {{name}}hallintapyyntö", + "releaseControl": "Vapautusohjaus", + "grantControl": "Anna hallinta", + "revokeControl": "Ota hallinta takaisin", + "controlRequestedBy": "{{name}} pyysi hallintaoikeutta", + "grant": "Myöntää", + "controlBadge": "Hallinnassa", + "youArePresenting": "Esität toisella välilehdellä. Lopeta esitys luovuttaaksesi esiintymispaikan.", + "created": "Huone luotu", + "invited": "Jäsenet kutsuttuina", + "roomTab": "Kokous" + }, + "nav": { + "collab": "Kokoukset" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Salli istunnon jakaminen", + "description": "Salli reaaliaikaisten pääte-, RDP-, VNC- ja Telnet-istuntojen jakaminen koko instanssin kesken. Poistamalla tämän asetuksen käytöstä ohitetaan kaikki isäntäkohtaisesti jakamiseen liittyvät asetukset." + } + }, + "host": { + "allowSessionSharing": { + "label": "Salli istunnon jakaminen", + "description": "Salli live-istuntojen jakaminen tällä isännällä linkin kautta tai muiden käyttäjien kanssa" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/fr_FR.json b/plugins/session-sharing/locales/translated/fr_FR.json new file mode 100644 index 000000000..3b3f4178b --- /dev/null +++ b/plugins/session-sharing/locales/translated/fr_FR.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Connexion à la session partagée...", + "linkInvalid": "Ce lien de partage est invalide, a expiré ou a été révoqué.", + "rateLimited": "Trop de tentatives, veuillez réessayer dans quelques instants.", + "sessionEnded": "Cette session est terminée.", + "readOnlyBadge": "Affichage uniquement", + "ownerLabel": "propriétaire de l'hôte" + }, + "modalTitle": "Session de partage", + "shareButton": "Partager", + "notReadyToShare": "La session n'est pas encore prête à être partagée.", + "modeTab": { + "link": "Lien", + "user": "Utilisateur" + }, + "linkModeDescription": "Toute personne disposant de ce lien peut s'inscrire, aucun compte n'est requis.", + "userModeDescription": "Partagez cette session avec un utilisateur spécifique qui y a déjà accès. S'il n'y a pas encore accès, partagez-lui d'abord l'hôte ou utilisez un lien. Une fois partagée, la session apparaîtra dans son onglet Connexions.", + "permissionLevel": { + "label": "Niveau d'autorisation", + "readOnly": "Lecture seule", + "readOnlyDescription": "Je peux suivre la session en direct, mais je ne peux ni écrire ni interagir.", + "readWrite": "Lecture-écriture", + "readWriteDescription": "Peut saisir du texte et interagir avec la session comme le propriétaire." + }, + "expiryLabel": "Expiration du lien", + "createLinkButton": "Créer un lien", + "createShareButton": "Partager avec l'utilisateur", + "searchUsersPlaceholder": "Rechercher des utilisateurs...", + "noUsersFound": "Aucun utilisateur trouvé", + "linkCreated": "Lien de partage créé", + "linkCopied": "Lien copié dans le presse-papiers", + "copyLink": "Copier le lien", + "shareCreated": "Session partagée. Elle apparaîtra dans leur onglet Connexions.", + "shareFailed": "Échec de la création du partage", + "userLacksHostAccess": "Cet utilisateur n'a pas encore accès à cet hôte. Veuillez d'abord lui partager l'hôte ou utiliser un lien.", + "activeShares": "Actions actives", + "noActiveShares": "Aucune action active pour cette session", + "revoke": "Révoquer", + "revokeConfirmTitle": "Révoquer cette action ?", + "revokeConfirmDescription": "Toute personne utilisant ce partage perdra immédiatement l'accès.", + "revoked": "Action révoquée", + "revokeFailed": "Échec de la révocation de l'action", + "joinCount": "{{count}} rejoindre", + "joinCount_other": "{{count}} rejoint", + "expiresAt": "Expire {{date}}", + "linkShareBadge": "Lien", + "userShareBadge": "Utilisateur : {{username}}", + "loadSharesFailed": "Échec du chargement des actions actives", + "customHoursPlaceholder": "Heures jusqu'à l'expiration de l'accès" + }, + "collab": { + "createRoom": "Nouvelle chambre", + "roomName": "Nom de la chambre", + "persistentRoom": "Chambre persistante", + "persistentRoomHint": "Les salles persistantes restent répertoriées après la fin de la réunion et peuvent être réutilisées.", + "noRooms": "Aucune salle disponible pour le moment. Créez-en une pour démarrer une réunion.", + "openRoom": "Ouvrir", + "endRoom": "Fin de la réunion", + "leaveRoom": "Partir", + "deleteMember": "Retirer", + "removeMember": "Supprimer {{name}}", + "removeMemberTitle": "Supprimer ce membre ?", + "removeMemberDescription": "{{name}} perdra immédiatement l'accès à cette salle. Tout utilisateur SSH actif sera déconnecté.", + "invite": "Inviter", + "inviteTitle": "Inviter des membres", + "members": "Membres", + "membersPanel": "Membres de la salle et demandes de contrôle", + "membersWithCount": "Membres ({{count}})", + "online": "en ligne", + "offline": "hors ligne", + "you": "(toi)", + "hostBadge": "Hôte", + "presenterBadge": "Présentation", + "present": "Présent", + "presentTitle": "Montez sur scène", + "presentHost": "Hôte", + "searchHosts": "Rechercher des hôtes", + "noHostsFound": "Aucun hôte disponible trouvé.", + "presentProtocol": "Protocole", + "stopPresenting": "Arrêtez de présenter", + "takeOver": "Reprendre", + "takeOverConfirmTitle": "Remplacer la présentation actuelle ?", + "takeOverDescription": "{{name}} est en train de présenter. Prendre le relais déconnectera tout le monde de cette scène.", + "emptyStage": "Personne ne présente. Montez sur scène pour animer une session.", + "stageLoading": "Se connecter à la scène...", + "stageEnded": "La présentation s'est terminée.", + "roomEnded": "Cette réunion est terminée.", + "presenterLabel": "{{name}} présente", + "guestLink": "Lien invité", + "guestLinkOn": "Le lien invité est activé. Toute personne disposant du lien peut assister à la représentation.", + "guestLinkOff": "Le lien invité est désactivé.", + "copyLink": "Copier le lien", + "rotateLink": "Lien de rotation", + "disableLink": "Désactiver le lien", + "rotateLinkConfirmTitle": "Faire pivoter le lien invité ?", + "rotateLinkDescription": "La connexion actuelle sera immédiatement interrompue. Les sessions SSH virtuelles seront déconnectées.", + "disableLinkConfirmTitle": "Désactiver le lien invité ?", + "disableLinkDescription": "Les utilisateurs anonymes utilisant SSH seront immédiatement déconnectés et le lien actuel cessera de fonctionner.", + "linkCopied": "Lien copié", + "roles": "Rôles", + "users": "Utilisateurs", + "searchUsers": "Utilisateurs de recherche", + "endConfirmTitle": "Mettre fin à cette réunion ?", + "endDescription": "{{name}} prendra fin pour tout le monde et ne pourra pas être rouvert.", + "endPersistentDescription": "La présentation en cours dans {{name}} va s'arrêter. La salle reste disponible pour une réutilisation.", + "deleteRoom": "Supprimer la salle", + "deleteConfirmTitle": "Supprimer cette pièce ?", + "deleteDescription": "{{name}} sera définitivement supprimé pour tout le monde et ne pourra pas être annulé.", + "invitedTo": "Vous avez été invité à \"{{name}}\"", + "guest": { + "title": "Réunion", + "waiting": "En attente du présentateur...", + "linkInvalid": "Ce lien invité est invalide ou la réunion est terminée." + }, + "reopenFromPanel": "Cette réunion a expiré. Veuillez rouvrir la salle depuis le panneau Réunions.", + "requestControl": "Contrôle des demandes", + "cancelControlRequest": "Demande d'annulation", + "controlQueue": "Demandes de contrôle", + "noControlRequests": "Aucune requête en attente.", + "grantControlTo": "Donnez le contrôle à {{name}}", + "dismissControlRequest": "Rejeter la demande de contrôle de {{name}}", + "releaseControl": "Contrôle de la libération", + "grantControl": "Donnez le contrôle", + "revokeControl": "Reprenez le contrôle", + "controlRequestedBy": "{{name}} a demandé le contrôle", + "grant": "Accorder", + "controlBadge": "En contrôle", + "youArePresenting": "Vous présentez dans un autre onglet. Arrêtez votre présentation pour laisser la parole à quelqu'un d'autre.", + "created": "Pièce créée", + "invited": "Les membres invités", + "roomTab": "Réunion" + }, + "nav": { + "collab": "Réunions" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Autoriser le partage de session", + "description": "Autorise le partage des sessions de terminal en direct, RDP, VNC et Telnet à l'échelle de l'instance. Remplace toute option de partage par hôte lorsqu'elle est désactivée." + } + }, + "host": { + "allowSessionSharing": { + "label": "Autoriser le partage de session", + "description": "Autoriser le partage des sessions en direct sur cet hébergeur via un lien ou avec d'autres utilisateurs" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/he_IL.json b/plugins/session-sharing/locales/translated/he_IL.json new file mode 100644 index 000000000..a05781856 --- /dev/null +++ b/plugins/session-sharing/locales/translated/he_IL.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "מתחבר לסשן משותף...", + "linkInvalid": "קישור השיתוף הזה אינו תקין, פג תוקף או בוטל", + "rateLimited": "יותר מדי ניסיונות, אנא נסה שוב בקרוב", + "sessionEnded": "מושב זה הסתיים", + "readOnlyBadge": "צפייה בלבד", + "ownerLabel": "בעל המארח" + }, + "modalTitle": "שיתוף סשן", + "shareButton": "לַחֲלוֹק", + "notReadyToShare": "הסשן עדיין לא מוכן לשיתוף", + "modeTab": { + "link": "לְקַשֵׁר", + "user": "מִשׁתַמֵשׁ" + }, + "linkModeDescription": "כל מי שיש לו קישור זה יכול להצטרף, אין צורך בחשבון.", + "userModeDescription": "שתף עם משתמש ספציפי שכבר יש לו גישה למארח זה. אם עדיין אין לו גישה, שתף איתו תחילה את המארח או השתמש בקישור במקום זאת. לאחר השיתוף, הסשן מופיע בכרטיסייה \"חיבורים\" שלו.", + "permissionLevel": { + "label": "רמת הרשאה", + "readOnly": "לקריאה בלבד", + "readOnlyDescription": "יכול לצפות בפגישה בשידור חי אך לא להקליד או לתקשר.", + "readWrite": "קריאה-כתיבה", + "readWriteDescription": "יכול להקליד ולקיים אינטראקציה עם הסשן בדיוק כמו הבעלים." + }, + "expiryLabel": "תוקף הקישור", + "createLinkButton": "צור קישור", + "createShareButton": "שתף עם המשתמש", + "searchUsersPlaceholder": "חיפוש משתמשים...", + "noUsersFound": "לא נמצאו משתמשים", + "linkCreated": "קישור השיתוף נוצר", + "linkCopied": "קישור שהועתק ללוח", + "copyLink": "העתקת קישור", + "shareCreated": "הסשן שותף. זה יופיע בלשונית החיבורים שלהם.", + "shareFailed": "יצירת השיתוף נכשלה", + "userLacksHostAccess": "למשתמש הזה עדיין אין גישה למארח הזה. שתף את המארח איתו תחילה, או השתמש בקישור במקום זאת.", + "activeShares": "מניות פעילות", + "noActiveShares": "אין שיתופים פעילים עבור סשן זה", + "revoke": "לְבַטֵל", + "revokeConfirmTitle": "לבטל את השיתוף הזה?", + "revokeConfirmDescription": "כל מי שישתמש בשיתוף הזה יאבד גישה באופן מיידי.", + "revoked": "השיתוף בוטל", + "revokeFailed": "ביטול השיתוף נכשל", + "joinCount": "{{count}} join", + "joinCount_other": "{{count}} מצטרף", + "expiresAt": "פג תוקף {{date}}", + "linkShareBadge": "לְקַשֵׁר", + "userShareBadge": "משתמש: {{username}}", + "loadSharesFailed": "נכשלה טעינת השיתופים הפעילים", + "customHoursPlaceholder": "שעות עד לפקיעת הגישה" + }, + "collab": { + "createRoom": "חדר חדש", + "roomName": "שם החדר", + "persistentRoom": "חדר קבוע", + "persistentRoomHint": "חדרים קבועים נשארים רשומים לאחר סיום הפגישה וניתן לעשות בהם שימוש חוזר.", + "noRooms": "אין עדיין חדרים. צור אחד כדי להתחיל פגישה.", + "openRoom": "לִפְתוֹחַ", + "endRoom": "סיום פגישה", + "leaveRoom": "לַעֲזוֹב", + "deleteMember": "לְהַסִיר", + "removeMember": "הסר {{name}}", + "removeMemberTitle": "להסיר את החבר הזה?", + "removeMemberDescription": "{{name}} יאבד גישה לחדר הזה באופן מיידי. כל צופה פעיל ב-SSH ינותק.", + "invite": "לְהַזמִין", + "inviteTitle": "הזמנת חברים", + "members": "חברים", + "membersPanel": "חברי חדר ובקשות בקרה", + "membersWithCount": "חברים ({{count}})", + "online": "באינטרנט", + "offline": "לא מקוון", + "you": "(אַתָה)", + "hostBadge": "מְאָרֵחַ", + "presenterBadge": "הצגה", + "present": "לְהַצִיג", + "presentTitle": "עלו על הבמה", + "presentHost": "מְאָרֵחַ", + "searchHosts": "חיפוש מארחים", + "noHostsFound": "לא נמצאו מארחים זמינים.", + "presentProtocol": "פּרוֹטוֹקוֹל", + "stopPresenting": "הפסקת ההצגה", + "takeOver": "השתלטות", + "takeOverConfirmTitle": "להחליף את המצגת הנוכחית?", + "takeOverDescription": "{{name}} מציג כעת. השתלטות תנתק את כולם מהבמה הזו.", + "emptyStage": "אף אחד לא מציג. עלו לבמה כדי לשתף סשן.", + "stageLoading": "מתחבר לבמה...", + "stageEnded": "המצגת הסתיימה", + "roomEnded": "פגישה זו הסתיימה", + "presenterLabel": "{{name}} מציג", + "guestLink": "קישור אורח", + "guestLinkOn": "קישור האורח פעיל. כל מי שיש לו קישור יכול לצפות בבמה.", + "guestLinkOff": "קישור האורח כבוי.", + "copyLink": "העתקת קישור", + "rotateLink": "סיבוב קישור", + "disableLink": "השבת קישור", + "rotateLinkConfirmTitle": "לסובב את קישור האורח?", + "rotateLinkDescription": "הקישור הנוכחי יפסיק לפעול באופן מיידי. אורחי SSH המחוברים ינותקו.", + "disableLinkConfirmTitle": "להשבית את קישור האורח?", + "disableLinkDescription": "צופי SSH אנונימיים ינותקו באופן מיידי והקישור הנוכחי יפסיק לפעול.", + "linkCopied": "הקישור הועתק", + "roles": "תפקידים", + "users": "משתמשים", + "searchUsers": "חיפוש משתמשים", + "endConfirmTitle": "לסיים את הפגישה הזו?", + "endDescription": "{{name}} יסתיים עבור כולם ולא ניתן יהיה לפתוח אותו מחדש.", + "endPersistentDescription": "המצגת הפעילה ב- {{name}} תיעצר. החדר יישאר זמין לשימוש חוזר.", + "deleteRoom": "מחיקת חדר", + "deleteConfirmTitle": "למחוק את החדר הזה?", + "deleteDescription": "{{name}} יימחק לצמיתות עבור כולם ולא ניתן יהיה לבטל אותו.", + "invitedTo": "הוזמנת ל-\"{{name}}\"", + "guest": { + "title": "פְּגִישָׁה", + "waiting": "מחכה למנחה...", + "linkInvalid": "קישור האורח הזה אינו תקין או שהפגישה הסתיימה" + }, + "reopenFromPanel": "פג תוקף כרטיסיית הפגישה הזו. פתח מחדש את החדר מחלונית הפגישות.", + "requestControl": "בקרת בקשות", + "cancelControlRequest": "ביטול בקשה", + "controlQueue": "בקשות בקרה", + "noControlRequests": "אין בקשות ממתינות.", + "grantControlTo": "תן שליטה ל- {{name}}", + "dismissControlRequest": "סגור את בקשת הבקרה של {{name}}", + "releaseControl": "בקרת שחרור", + "grantControl": "תן שליטה", + "revokeControl": "קח בחזרה את השליטה", + "controlRequestedBy": "{{name}} ביקש שליטה", + "grant": "מַעֲנָק", + "controlBadge": "בשליטה", + "youArePresenting": "אתה מציג בכרטיסייה אחרת. הפסק את ההצגה כדי להעביר את הבמה.", + "created": "החדר נוצר", + "invited": "חברים שהוזמנו", + "roomTab": "פְּגִישָׁה" + }, + "nav": { + "collab": "פגישות" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "אפשר שיתוף סשנים", + "description": "מאפשר שיתוף של הפעלות מסוף חי, RDP, VNC ו-Telnet בכל המופעים. מבטל כל אפשרות שיתוף לפי מארח כאשר היא מושבתת." + } + }, + "host": { + "allowSessionSharing": { + "label": "אפשר שיתוף סשנים", + "description": "אפשר שיתוף של מפגשים חיים במארח זה באמצעות קישור או עם משתמשים אחרים" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/hi_IN.json b/plugins/session-sharing/locales/translated/hi_IN.json new file mode 100644 index 000000000..3464f12a5 --- /dev/null +++ b/plugins/session-sharing/locales/translated/hi_IN.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "साझा सत्र से जुड़ रहा है...", + "linkInvalid": "यह शेयर लिंक अमान्य है, इसकी समय सीमा समाप्त हो चुकी है, या इसे रद्द कर दिया गया है।", + "rateLimited": "बहुत अधिक प्रयास किए गए, कृपया कुछ देर बाद पुनः प्रयास करें।", + "sessionEnded": "यह सत्र समाप्त हो गया है।", + "readOnlyBadge": "केवल देखें", + "ownerLabel": "मेज़बान स्वामी" + }, + "modalTitle": "सत्र साझा करें", + "shareButton": "शेयर करना", + "notReadyToShare": "सेशन अभी शेयर करने के लिए तैयार नहीं है", + "modeTab": { + "link": "जोड़ना", + "user": "उपयोगकर्ता" + }, + "linkModeDescription": "इस लिंक के जरिए कोई भी जुड़ सकता है, इसके लिए किसी खाते की आवश्यकता नहीं है।", + "userModeDescription": "इसे उस विशिष्ट उपयोगकर्ता के साथ साझा करें जिसके पास पहले से ही इस होस्ट का एक्सेस है। यदि उनके पास अभी एक्सेस नहीं है, तो पहले उनके साथ होस्ट साझा करें या इसके बजाय एक लिंक का उपयोग करें। साझा करने के बाद, सेशन उनके कनेक्शन टैब में दिखाई देगा।", + "permissionLevel": { + "label": "अनुमति स्तर", + "readOnly": "केवल पढ़ने के लिए", + "readOnlyDescription": "आप सेशन को लाइव देख सकते हैं लेकिन टाइप या इंटरैक्ट नहीं कर सकते।", + "readWrite": "पढ़ना-लिखना", + "readWriteDescription": "मालिक की तरह ही सेशन में टाइप कर सकता है और इंटरैक्ट कर सकता है।" + }, + "expiryLabel": "लिंक की समय सीमा समाप्त", + "createLinkButton": "लिंक बनाएं", + "createShareButton": "उपयोगकर्ता के साथ साझा करें", + "searchUsersPlaceholder": "उपयोगकर्ताओं को खोजें...", + "noUsersFound": "कोई उपयोगकर्ता नहीं मिला", + "linkCreated": "शेयर लिंक बनाया गया", + "linkCopied": "लिंक क्लिपबोर्ड पर कॉपी हो गया है", + "copyLink": "लिंक की प्रतिलिपि करें", + "shareCreated": "सेशन साझा कर दिया गया है। यह उनके कनेक्शन टैब में दिखाई देगा।", + "shareFailed": "शेयर बनाने में विफल", + "userLacksHostAccess": "उस उपयोगकर्ता के पास अभी इस होस्ट तक पहुंच नहीं है। कृपया पहले उनके साथ होस्ट साझा करें, या इसके बजाय एक लिंक का उपयोग करें।", + "activeShares": "सक्रिय शेयर", + "noActiveShares": "इस सत्र के लिए कोई सक्रिय शेयर नहीं हैं", + "revoke": "रद्द करना", + "revokeConfirmTitle": "क्या इस शेयर को रद्द कर दें?", + "revokeConfirmDescription": "इस शेयर का उपयोग करने वाले किसी भी व्यक्ति की पहुंच तुरंत समाप्त हो जाएगी।", + "revoked": "शेयर रद्द कर दिया गया", + "revokeFailed": "शेयर रद्द करने में विफल", + "joinCount": "{{count}} जुड़ें", + "joinCount_other": "{{count}} जुड़ता है", + "expiresAt": "समाप्ति तिथि {{date}}", + "linkShareBadge": "जोड़ना", + "userShareBadge": "उपयोगकर्ता: {{username}}", + "loadSharesFailed": "सक्रिय शेयर लोड करने में विफल", + "customHoursPlaceholder": "पहुँच समाप्त होने तक के घंटे" + }, + "collab": { + "createRoom": "नया कमरा", + "roomName": "कमरे का नाम", + "persistentRoom": "लगातार कमरा", + "persistentRoomHint": "स्थायी कमरे मीटिंग समाप्त होने के बाद भी सूची में बने रहते हैं और उनका पुनः उपयोग किया जा सकता है।", + "noRooms": "अभी कोई कमरा उपलब्ध नहीं है। मीटिंग शुरू करने के लिए एक कमरा बनाएं।", + "openRoom": "खुला", + "endRoom": "बैठक समाप्त", + "leaveRoom": "छुट्टी", + "deleteMember": "निकालना", + "removeMember": "{{name}} हटाएँ", + "removeMemberTitle": "क्या इस सदस्य को हटा दें?", + "removeMemberDescription": "{{name}} को इस कमरे तक पहुँच तुरंत खोनी पड़ेगी। कोई भी सक्रिय SSH व्यूअर डिस्कनेक्ट हो जाएगा।", + "invite": "आमंत्रित करना", + "inviteTitle": "सदस्यों को आमंत्रित करो", + "members": "सदस्यों", + "membersPanel": "कमरे के सदस्य और नियंत्रण अनुरोध", + "membersWithCount": "सदस्य ({{count}})", + "online": "ऑनलाइन", + "offline": "ऑफलाइन", + "you": "(आप)", + "hostBadge": "मेज़बान", + "presenterBadge": "प्रस्तुत करते हुए", + "present": "उपस्थित", + "presentTitle": "मंच पर आइए", + "presentHost": "मेज़बान", + "searchHosts": "होस्ट खोजें", + "noHostsFound": "कोई उपलब्ध होस्ट नहीं मिला।", + "presentProtocol": "शिष्टाचार", + "stopPresenting": "प्रस्तुति देना बंद करो", + "takeOver": "कब्जा", + "takeOverConfirmTitle": "क्या वर्तमान प्रस्तुति को बदलना है?", + "takeOverDescription": "{{name}} अभी प्रस्तुति दे रहा है। कार्यभार संभालने से सभी लोग उस मंच से अलग हो जाएंगे।", + "emptyStage": "आज कोई प्रस्तुति नहीं दे रहा है। आप मंच पर आकर अपना सत्र साझा करें।", + "stageLoading": "मंच से जुड़ना...", + "stageEnded": "प्रस्तुति समाप्त हुई", + "roomEnded": "यह बैठक समाप्त हो गई है", + "presenterLabel": "{{name}} प्रस्तुत कर रहा है", + "guestLink": "अतिथि लिंक", + "guestLinkOn": "अतिथि लिंक चालू है। लिंक के माध्यम से कोई भी व्यक्ति मंच देख सकता है।", + "guestLinkOff": "अतिथि लिंक बंद है।", + "copyLink": "लिंक की प्रतिलिपि करें", + "rotateLink": "लिंक घुमाएँ", + "disableLink": "लिंक अक्षम करें", + "rotateLinkConfirmTitle": "अतिथि लिंक को घुमाएँ?", + "rotateLinkDescription": "वर्तमान लिंक तुरंत काम करना बंद कर देगा। कनेक्टेड एसएसएच गेस्ट डिस्कनेक्ट हो जाएंगे।", + "disableLinkConfirmTitle": "अतिथि लिंक को निष्क्रिय करें?", + "disableLinkDescription": "अनाम एसएसएच व्यूअर्स तुरंत डिस्कनेक्ट हो जाएंगे और वर्तमान लिंक काम करना बंद कर देगा।", + "linkCopied": "लिंक कॉपी हो गया", + "roles": "भूमिकाएँ", + "users": "उपयोगकर्ताओं", + "searchUsers": "उपयोगकर्ताओं को खोजें", + "endConfirmTitle": "क्या इस बैठक को यहीं समाप्त करें?", + "endDescription": "{{name}} सभी के लिए समाप्त हो जाएगा और इसे दोबारा नहीं खोला जा सकता है।", + "endPersistentDescription": "{{name}} में चल रही प्रस्तुति बंद हो जाएगी। कमरा पुनः उपयोग के लिए उपलब्ध रहेगा।", + "deleteRoom": "कमरा हटाएं", + "deleteConfirmTitle": "क्या इस कमरे को हटा दें?", + "deleteDescription": "{{name}} को सभी के लिए स्थायी रूप से हटा दिया जाएगा और इसे पूर्ववत नहीं किया जा सकता है।", + "invitedTo": "आपको \"{{name}} \" में आमंत्रित किया गया था", + "guest": { + "title": "बैठक", + "waiting": "प्रस्तुतकर्ता का इंतजार है...", + "linkInvalid": "यह अतिथि लिंक अमान्य है या मीटिंग समाप्त हो चुकी है" + }, + "reopenFromPanel": "इस मीटिंग टैब की समय सीमा समाप्त हो गई है। कृपया मीटिंग पैनल से कमरे को पुनः खोलें।", + "requestControl": "अनुरोध नियंत्रण", + "cancelControlRequest": "अनुरोध को रद्द करें", + "controlQueue": "नियंत्रण अनुरोध", + "noControlRequests": "कोई लंबित अनुरोध नहीं।", + "grantControlTo": "{{name}} को नियंत्रण दें", + "dismissControlRequest": "{{name}}के नियंत्रण अनुरोध को खारिज करें", + "releaseControl": "नियंत्रण छोड़ें", + "grantControl": "नियंत्रण सौंप दें", + "revokeControl": "नियंत्रण वापस अपने हाथ में लें।", + "controlRequestedBy": "{{name}} ने नियंत्रण मांगा", + "grant": "अनुदान", + "controlBadge": "नियंत्रण में", + "youArePresenting": "आप किसी दूसरे टैब में प्रेजेंटेशन दे रहे हैं। कृपया प्रेजेंटेशन देना बंद करें और स्टेज किसी और को सौंप दें।", + "created": "कमरा बनाया गया", + "invited": "सदस्यों को आमंत्रित किया गया", + "roomTab": "बैठक" + }, + "nav": { + "collab": "बैठक" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "सेशन शेयरिंग की अनुमति दें", + "description": "लाइव टर्मिनल, RDP, VNC और Telnet सेशन को पूरे इंस्टेंस में साझा करने की अनुमति दें। अक्षम होने पर यह प्रति-होस्ट साझाकरण टॉगल को ओवरराइड कर देता है।" + } + }, + "host": { + "allowSessionSharing": { + "label": "सेशन शेयरिंग की अनुमति दें", + "description": "इस होस्ट पर होने वाले लाइव सेशन को लिंक के माध्यम से या अन्य उपयोगकर्ताओं के साथ साझा किया जा सकता है।" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/hu_HU.json b/plugins/session-sharing/locales/translated/hu_HU.json new file mode 100644 index 000000000..a0832e55a --- /dev/null +++ b/plugins/session-sharing/locales/translated/hu_HU.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Kapcsolódás a megosztott munkamenethez...", + "linkInvalid": "Ez a megosztási link érvénytelen, lejárt vagy visszavonták.", + "rateLimited": "Túl sok próbálkozás, kérjük, próbáld újra később", + "sessionEnded": "Ez a munkamenet véget ért", + "readOnlyBadge": "Csak megtekintés", + "ownerLabel": "Házigazda tulajdonos" + }, + "modalTitle": "Megosztási munkamenet", + "shareButton": "Részesedés", + "notReadyToShare": "A munkamenet még nem megosztható", + "modeTab": { + "link": "Link", + "user": "Felhasználó" + }, + "linkModeDescription": "Bárki csatlakozhat a link birtokában, nincs szükség regisztrációra.", + "userModeDescription": "Megosztás egy adott felhasználóval, aki már rendelkezik hozzáféréssel ehhez a gazdagéphez. Ha még nincs hozzáférésük, először oszd meg vele a gazdagépet, vagy használj hivatkozást. A megosztás után a munkamenet megjelenik a Kapcsolatok lapon.", + "permissionLevel": { + "label": "Jogosultsági szint", + "readOnly": "Csak olvasható", + "readOnlyDescription": "Élőben nézhetem az előadást, de nem gépelhetek és nem léphetek interakcióba.", + "readWrite": "Olvasható-írható", + "readWriteDescription": "Pont úgy tud gépelni és kommunikálni a munkamenettel, mint a tulajdonos." + }, + "expiryLabel": "Link lejárata", + "createLinkButton": "Link létrehozása", + "createShareButton": "Megosztás a felhasználóval", + "searchUsersPlaceholder": "Felhasználók keresése...", + "noUsersFound": "Nem található felhasználó", + "linkCreated": "Megosztási link létrehozva", + "linkCopied": "Link a vágólapra másolva", + "copyLink": "Link másolása", + "shareCreated": "Munkamenet megosztva. Megjelenik a Kapcsolatok fülön.", + "shareFailed": "Nem sikerült létrehozni a megosztást", + "userLacksHostAccess": "A felhasználónak még nincs hozzáférése ehhez a gazdagéphez. Először oszd meg vele a gazdagépet, vagy használj linket.", + "activeShares": "Aktív megosztások", + "noActiveShares": "Nincsenek aktív megosztások ebben a munkamenetben", + "revoke": "Visszavonás", + "revokeConfirmTitle": "Visszavonja ezt a megosztást?", + "revokeConfirmDescription": "Bárki, aki ezt a megosztást használja, azonnal elveszíti a hozzáférését.", + "revoked": "Megosztás visszavonva", + "revokeFailed": "Nem sikerült visszavonni a megosztást", + "joinCount": "{{count}} csatlakozás", + "joinCount_other": "{{count}} csatlakozások", + "expiresAt": "Lejár {{date}}", + "linkShareBadge": "Link", + "userShareBadge": "Felhasználó: {{username}}", + "loadSharesFailed": "Nem sikerült betölteni az aktív megosztásokat", + "customHoursPlaceholder": "Órák a hozzáférés lejártáig" + }, + "collab": { + "createRoom": "Új szoba", + "roomName": "Szoba neve", + "persistentRoom": "Állandó szoba", + "persistentRoomHint": "Az állandó szobák a megbeszélés befejezése után is listázva maradnak, és újra felhasználhatók.", + "noRooms": "Még nincsenek szobák. Hozz létre egyet a megbeszélés megkezdéséhez.", + "openRoom": "Nyitott", + "endRoom": "Megbeszélés vége", + "leaveRoom": "Szabadság", + "deleteMember": "Eltávolítás", + "removeMember": "Eltávolítás {{name}}", + "removeMemberTitle": "Eltávolítja ezt a tagot?", + "removeMemberDescription": "{{name}} azonnal elveszíti a hozzáférést ehhez a szobához. Minden aktív SSH-megjelenítő lekapcsolódik.", + "invite": "Meghív", + "inviteTitle": "Tagok meghívása", + "members": "Tagok", + "membersPanel": "Szoba tagjai és irányítási kérelmek", + "membersWithCount": "Tagok ({{count}})", + "online": "online", + "offline": "offline", + "you": "(te)", + "hostBadge": "Házigazda", + "presenterBadge": "Bemutatja", + "present": "Jelenlegi", + "presentTitle": "Lépj színpadra", + "presentHost": "Házigazda", + "searchHosts": "Keresés hosztok", + "noHostsFound": "Nem található elérhető host.", + "presentProtocol": "Jegyzőkönyv", + "stopPresenting": "Prezentáció leállítása", + "takeOver": "Átvétel", + "takeOverConfirmTitle": "Lecseréli a jelenlegi prezentációt?", + "takeOverDescription": "{{name}} most tart előadást. Az irányítás átvétele mindenkit lekapcsolna arról a színpadról.", + "emptyStage": "Senki sem tart előadást. Lépjen színpadra, és ossza meg velünk az előadást.", + "stageLoading": "Csatlakozás a színpadhoz...", + "stageEnded": "A prezentáció véget ért", + "roomEnded": "Ez a találkozó véget ért", + "presenterLabel": "{{name}} bemutatja", + "guestLink": "Vendéglink", + "guestLinkOn": "Vendéglink be van kapcsolva. Bárki, aki rendelkezik a linkkel, nézheti a színpadot.", + "guestLinkOff": "A vendéglink ki van kapcsolva.", + "copyLink": "Link másolása", + "rotateLink": "Link elforgatása", + "disableLink": "Link letiltása", + "rotateLinkConfirmTitle": "Vendéglink elforgatása?", + "rotateLinkDescription": "A jelenlegi kapcsolat azonnal leáll. A csatlakoztatott SSH-vendégek le lesznek választva.", + "disableLinkConfirmTitle": "Letiltja a vendéglinket?", + "disableLinkDescription": "A névtelen SSH-nézők azonnal le lesznek választva, és a jelenlegi link nem fog működni.", + "linkCopied": "Link másolva", + "roles": "Szerepkörök", + "users": "Felhasználók", + "searchUsers": "Felhasználók keresése", + "endConfirmTitle": "Befejezi ezt a megbeszélést?", + "endDescription": "{{name}} mindenki számára megszűnik, és nem lehet újra megnyitni.", + "endPersistentDescription": "A {{name}} teremben futó aktív prezentáció leáll. A szoba továbbra is elérhető marad újrafelhasználásra.", + "deleteRoom": "Szoba törlése", + "deleteConfirmTitle": "Törli ezt a szobát?", + "deleteDescription": "A {{name}} véglegesen törlésre kerül mindenki számára, és a művelet nem vonható vissza.", + "invitedTo": "Meghívtak a következőre: \"{{name}}\"", + "guest": { + "title": "Találkozó", + "waiting": "Várjuk az előadót...", + "linkInvalid": "Ez a vendéglink érvénytelen, vagy a megbeszélés véget ért." + }, + "reopenFromPanel": "Ez a megbeszélésfül lejárt. Nyissa meg újra a szobát a Megbeszélések panelről.", + "requestControl": "Kérelemvezérlés", + "cancelControlRequest": "Kérés visszavonása", + "controlQueue": "Ellenőrzési kérelmek", + "noControlRequests": "Nincsenek függőben lévő kérések.", + "grantControlTo": "Átadni az irányítást {{name}}-nak", + "dismissControlRequest": "Elutasítom {{name}}vezérlési kérelmét", + "releaseControl": "Kioldásvezérlés", + "grantControl": "Átadni az irányítást", + "revokeControl": "Vedd vissza az irányítást", + "controlRequestedBy": "{{name}} kérte az irányítást", + "grant": "Támogatás", + "controlBadge": "Irányítás alatt", + "youArePresenting": "Egy másik lapon tartasz előadást. Állítsa le az előadást, hogy átadhassa a helyet.", + "created": "Szoba létrehozva", + "invited": "Meghívott tagok", + "roomTab": "Találkozó" + }, + "nav": { + "collab": "Találkozók" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Munkamenet-megosztás engedélyezése", + "description": "Engedélyezi az élő terminál-, RDP-, VNC- és Telnet-munkamenetek példányonkénti megosztását. Letiltás esetén felülírja az összes gazdagépenkénti megosztási kapcsolót." + } + }, + "host": { + "allowSessionSharing": { + "label": "Munkamenet-megosztás engedélyezése", + "description": "Engedélyezi az élő munkamenetek megosztását ezen a gazdagépen linken vagy más felhasználókkal" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/id_ID.json b/plugins/session-sharing/locales/translated/id_ID.json new file mode 100644 index 000000000..5022c36e8 --- /dev/null +++ b/plugins/session-sharing/locales/translated/id_ID.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Menghubungkan ke sesi bersama...", + "linkInvalid": "Tautan berbagi ini tidak valid, telah kedaluwarsa, atau telah dicabut.", + "rateLimited": "Terlalu banyak percobaan, silakan coba lagi sebentar lagi.", + "sessionEnded": "Sesi ini telah berakhir.", + "readOnlyBadge": "Hanya tampilan", + "ownerLabel": "Pemilik host" + }, + "modalTitle": "Sesi berbagi", + "shareButton": "Membagikan", + "notReadyToShare": "Sesi belum siap untuk dibagikan.", + "modeTab": { + "link": "Link", + "user": "Pengguna" + }, + "linkModeDescription": "Siapa pun yang memiliki tautan ini dapat bergabung, tidak perlu membuat akun.", + "userModeDescription": "Bagikan dengan pengguna tertentu yang sudah memiliki akses ke host ini. Jika mereka belum memiliki akses, bagikan host tersebut kepada mereka terlebih dahulu atau gunakan tautan. Setelah dibagikan, sesi akan muncul di tab Koneksi mereka.", + "permissionLevel": { + "label": "Tingkat izin", + "readOnly": "Hanya baca", + "readOnlyDescription": "Bisa menonton sesi secara langsung tetapi tidak bisa mengetik atau berinteraksi.", + "readWrite": "Baca-tulis", + "readWriteDescription": "Dapat mengetik dan berinteraksi dengan sesi seperti halnya pemilik." + }, + "expiryLabel": "Kedaluwarsa tautan", + "createLinkButton": "Buat tautan", + "createShareButton": "Bagikan dengan pengguna", + "searchUsersPlaceholder": "Cari pengguna...", + "noUsersFound": "Tidak ada pengguna yang ditemukan", + "linkCreated": "Tautan berbagi telah dibuat", + "linkCopied": "Tautan disalin ke papan klip", + "copyLink": "Salin tautan", + "shareCreated": "Sesi dibagikan. Sesi tersebut akan muncul di tab Koneksi mereka.", + "shareFailed": "Gagal membuat saham", + "userLacksHostAccess": "Pengguna tersebut belum memiliki akses ke host ini. Bagikan host tersebut kepada mereka terlebih dahulu, atau gunakan tautan sebagai gantinya.", + "activeShares": "Saham aktif", + "noActiveShares": "Tidak ada saham aktif untuk sesi ini.", + "revoke": "Menarik kembali", + "revokeConfirmTitle": "Batalkan pembagian ini?", + "revokeConfirmDescription": "Siapa pun yang menggunakan share ini akan langsung kehilangan akses.", + "revoked": "Saham dicabut", + "revokeFailed": "Gagal mencabut saham", + "joinCount": "{{count}} bergabung", + "joinCount_other": "{{count}} bergabung", + "expiresAt": "Berakhir {{date}}", + "linkShareBadge": "Link", + "userShareBadge": "Pengguna: {{username}}", + "loadSharesFailed": "Gagal memuat saham aktif.", + "customHoursPlaceholder": "Waktu hingga akses berakhir" + }, + "collab": { + "createRoom": "Kamar baru", + "roomName": "Nama ruangan", + "persistentRoom": "Ruang permanen", + "persistentRoomHint": "Ruangan yang bersifat permanen akan tetap terdaftar setelah rapat berakhir dan dapat digunakan kembali.", + "noRooms": "Belum ada ruang rapat. Buat ruang rapat untuk memulai pertemuan.", + "openRoom": "Membuka", + "endRoom": "Mengakhiri pertemuan", + "leaveRoom": "Meninggalkan", + "deleteMember": "Menghapus", + "removeMember": "Hapus {{name}}", + "removeMemberTitle": "Hapus anggota ini?", + "removeMemberDescription": "{{name}} akan kehilangan akses ke ruangan ini segera. Setiap penonton SSH yang aktif akan terputus.", + "invite": "Mengundang", + "inviteTitle": "Undang anggota", + "members": "Anggota", + "membersPanel": "Anggota ruangan dan permintaan kontrol", + "membersWithCount": "Anggota ({{count}})", + "online": "on line", + "offline": "offline", + "you": "(Anda)", + "hostBadge": "Tuan rumah", + "presenterBadge": "Mempersembahkan", + "present": "Hadiah", + "presentTitle": "Naik ke panggung", + "presentHost": "Tuan rumah", + "searchHosts": "Cari host", + "noHostsFound": "Tidak ditemukan host yang tersedia.", + "presentProtocol": "Protokol", + "stopPresenting": "Berhenti melakukan presentasi", + "takeOver": "Mengambil alih", + "takeOverConfirmTitle": "Mengganti presentasi yang ada?", + "takeOverDescription": "{{name}} sedang melakukan presentasi sekarang. Mengambil alih akan memutuskan koneksi semua orang dari panggung tersebut.", + "emptyStage": "Tidak ada yang sedang presentasi. Silakan naik ke panggung untuk berbagi sesi.", + "stageLoading": "Terhubung ke panggung...", + "stageEnded": "Presentasi berakhir", + "roomEnded": "Pertemuan ini telah berakhir.", + "presenterLabel": "{{name}} sedang menampilkan", + "guestLink": "Tautan tamu", + "guestLinkOn": "Tautan tamu sudah aktif. Siapa pun yang memiliki tautan tersebut dapat menonton pertunjukan di panggung.", + "guestLinkOff": "Tautan tamu dinonaktifkan.", + "copyLink": "Salin tautan", + "rotateLink": "Putar tautan", + "disableLink": "Nonaktifkan tautan", + "rotateLinkConfirmTitle": "Putar tautan tamu?", + "rotateLinkDescription": "Koneksi saat ini akan segera berhenti berfungsi. Tamu SSH yang terhubung akan terputus.", + "disableLinkConfirmTitle": "Nonaktifkan tautan tamu?", + "disableLinkDescription": "Pengguna anonim yang mengakses melalui SSH akan langsung terputus dan tautan saat ini akan berhenti berfungsi.", + "linkCopied": "Tautan disalin", + "roles": "Peran", + "users": "Pengguna", + "searchUsers": "Cari pengguna", + "endConfirmTitle": "Apakah pertemuan ini bisa diakhiri?", + "endDescription": "{{name}} akan berakhir untuk semua orang dan tidak dapat dibuka kembali.", + "endPersistentDescription": "Presentasi aktif di {{name}} akan berhenti. Ruangan tetap tersedia untuk digunakan kembali.", + "deleteRoom": "Hapus ruangan", + "deleteConfirmTitle": "Hapus ruangan ini?", + "deleteDescription": "{{name}} akan dihapus secara permanen untuk semua orang dan tidak dapat dibatalkan.", + "invitedTo": "Anda diundang ke \"{{name}}\"", + "guest": { + "title": "Pertemuan", + "waiting": "Menunggu pembawa acara...", + "linkInvalid": "Tautan tamu ini tidak valid atau rapat telah berakhir." + }, + "reopenFromPanel": "Tab rapat ini telah kedaluwarsa. Buka kembali ruangan dari panel Rapat.", + "requestControl": "Kontrol permintaan", + "cancelControlRequest": "Batalkan permintaan", + "controlQueue": "Permintaan kontrol", + "noControlRequests": "Tidak ada permintaan yang tertunda.", + "grantControlTo": "Berikan kendali ke {{name}}", + "dismissControlRequest": "Abaikan permintaan kontrol {{name}}", + "releaseControl": "Lepaskan kontrol", + "grantControl": "Berikan kendali", + "revokeControl": "Ambil kembali kendali", + "controlRequestedBy": "{{name}} meminta kendali", + "grant": "Menganugerahkan", + "controlBadge": "Dalam kendali", + "youArePresenting": "Anda sedang melakukan presentasi di tab lain. Hentikan presentasi untuk memberikan kesempatan kepada saya.", + "created": "Ruangan dibuat", + "invited": "Anggota diundang", + "roomTab": "Pertemuan" + }, + "nav": { + "collab": "Pertemuan" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Izinkan Berbagi Sesi", + "description": "Izinkan sesi terminal langsung, RDP, VNC, dan Telnet untuk dibagikan di seluruh instance. Menggantikan setiap pengaturan berbagi per host saat dinonaktifkan." + } + }, + "host": { + "allowSessionSharing": { + "label": "Izinkan Berbagi Sesi", + "description": "Izinkan sesi langsung di host ini dibagikan melalui tautan atau dengan pengguna lain." + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/it_IT.json b/plugins/session-sharing/locales/translated/it_IT.json new file mode 100644 index 000000000..7858aa6c8 --- /dev/null +++ b/plugins/session-sharing/locales/translated/it_IT.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Connessione alla sessione condivisa in corso...", + "linkInvalid": "Questo link di condivisione non è valido, è scaduto o è stato revocato.", + "rateLimited": "Troppi tentativi, riprova tra poco.", + "sessionEnded": "Questa sessione è terminata", + "readOnlyBadge": "Solo visualizzazione", + "ownerLabel": "Proprietario dell'host" + }, + "modalTitle": "Sessione di condivisione", + "shareButton": "Condividere", + "notReadyToShare": "La sessione non è ancora pronta per la condivisione.", + "modeTab": { + "link": "Collegamento", + "user": "Utente" + }, + "linkModeDescription": "Chiunque abbia questo link può partecipare, non è necessario creare un account.", + "userModeDescription": "Condividi la sessione con un utente specifico che ha già accesso a questo host. Se non ha ancora accesso, condividi prima l'host con lui oppure usa un link. Una volta condivisa, la sessione apparirà nella sua scheda Connessioni.", + "permissionLevel": { + "label": "Livello di autorizzazione", + "readOnly": "Sola lettura", + "readOnlyDescription": "È possibile seguire la sessione in diretta, ma non è possibile digitare o interagire.", + "readWrite": "lettura-scrittura", + "readWriteDescription": "Può digitare e interagire con la sessione proprio come il proprietario." + }, + "expiryLabel": "Scadenza del collegamento", + "createLinkButton": "Crea collegamento", + "createShareButton": "Condividi con l'utente", + "searchUsersPlaceholder": "Cerca utenti...", + "noUsersFound": "Nessun utente trovato", + "linkCreated": "Link di condivisione creato", + "linkCopied": "Link copiato negli appunti", + "copyLink": "Copia il link", + "shareCreated": "Sessione condivisa. Sarà visibile nella scheda Connessioni.", + "shareFailed": "Impossibile creare la condivisione", + "userLacksHostAccess": "Quell'utente non ha ancora accesso a questo host. Condividi prima l'host con lui, oppure usa un link.", + "activeShares": "Condivisioni attive", + "noActiveShares": "Nessuna azione attiva in questa sessione.", + "revoke": "Revocare", + "revokeConfirmTitle": "Revocare questa condivisione?", + "revokeConfirmDescription": "Chiunque utilizzi questa condivisione perderà immediatamente l'accesso.", + "revoked": "Azione revocata.", + "revokeFailed": "Impossibile revocare la quota", + "joinCount": "{{count}} unisciti", + "joinCount_other": "{{count}} si unisce", + "expiresAt": "Scade il {{date}}", + "linkShareBadge": "Collegamento", + "userShareBadge": "Utente: {{username}}", + "loadSharesFailed": "Impossibile caricare le azioni attive", + "customHoursPlaceholder": "Ore fino alla scadenza dell'accesso" + }, + "collab": { + "createRoom": "Nuova stanza", + "roomName": "Nome della stanza", + "persistentRoom": "Stanza persistente", + "persistentRoomHint": "Le stanze persistenti rimangono elencate anche dopo la fine della riunione e possono essere riutilizzate.", + "noRooms": "Non ci sono ancora stanze. Creane una per avviare una riunione.", + "openRoom": "Aprire", + "endRoom": "Fine della riunione", + "leaveRoom": "Partire", + "deleteMember": "Rimuovere", + "removeMember": "Rimuovi {{name}}", + "removeMemberTitle": "Rimuovere questo membro?", + "removeMemberDescription": "{{name}} perderà immediatamente l'accesso a questa stanza. Qualsiasi visualizzatore SSH attivo verrà disconnesso.", + "invite": "Invitare", + "inviteTitle": "Invita i membri", + "members": "Membri", + "membersPanel": "Membri della stanza e richieste di controllo", + "membersWithCount": "Membri ({{count}})", + "online": "online", + "offline": "offline", + "you": "(Voi)", + "hostBadge": "Ospite", + "presenterBadge": "Presentazione", + "present": "Presente", + "presentTitle": "Sali sul palco", + "presentHost": "Ospite", + "searchHosts": "Host di ricerca", + "noHostsFound": "Nessun host disponibile trovato.", + "presentProtocol": "Protocollo", + "stopPresenting": "Smetti di presentare", + "takeOver": "Rilevare", + "takeOverConfirmTitle": "Sostituire la presentazione attuale?", + "takeOverDescription": "{{name}} sta presentando ora. Prendere il controllo disconnetterà tutti da quel palco.", + "emptyStage": "Nessuno sta presentando. Salite sul palco per condividere una sessione.", + "stageLoading": "Collegamento al palco...", + "stageEnded": "La presentazione è terminata", + "roomEnded": "La riunione è terminata", + "presenterLabel": "{{name}} sta presentando", + "guestLink": "Link ospite", + "guestLinkOn": "Il link per gli ospiti è attivo. Chiunque abbia il link può assistere alla diretta.", + "guestLinkOff": "Il link per gli ospiti è disattivato.", + "copyLink": "Copia il link", + "rotateLink": "Ruota il collegamento", + "disableLink": "Disabilita il collegamento", + "rotateLinkConfirmTitle": "Ruotare il link per gli ospiti?", + "rotateLinkDescription": "Il collegamento attuale smetterà di funzionare immediatamente. Gli ospiti SSH connessi verranno disconnessi.", + "disableLinkConfirmTitle": "Disabilitare il link per gli ospiti?", + "disableLinkDescription": "Gli utenti che tentano di connettersi tramite SSH in modo anonimo verranno disconnessi immediatamente e il collegamento corrente smetterà di funzionare.", + "linkCopied": "Link copiato", + "roles": "Ruoli", + "users": "Utenti", + "searchUsers": "Utenti di ricerca", + "endConfirmTitle": "Chiudere questa riunione?", + "endDescription": "{{name}} terminerà per tutti e non potrà essere riaperto.", + "endPersistentDescription": "La presentazione attiva in {{name}} verrà interrotta. La sala rimane disponibile per un altro utilizzo.", + "deleteRoom": "Eliminare la stanza", + "deleteConfirmTitle": "Eliminare questa stanza?", + "deleteDescription": "{{name}} verrà eliminato definitivamente per tutti e non sarà possibile annullare l'operazione.", + "invitedTo": "Sei stato invitato a \"{{name}}\"", + "guest": { + "title": "Incontro", + "waiting": "In attesa del presentatore...", + "linkInvalid": "Questo link per gli ospiti non è valido oppure la riunione è terminata." + }, + "reopenFromPanel": "La scheda di questa riunione è scaduta. Riapri la sala riunioni dal pannello Riunioni.", + "requestControl": "Controllo della richiesta", + "cancelControlRequest": "Annulla richiesta", + "controlQueue": "Richieste di controllo", + "noControlRequests": "Nessuna richiesta in sospeso.", + "grantControlTo": "Assegna il controllo a {{name}}", + "dismissControlRequest": "Ignora la richiesta di controllo di {{name}}", + "releaseControl": "Controllo del rilascio", + "grantControl": "Dare il controllo", + "revokeControl": "Riprendi il controllo", + "controlRequestedBy": "{{name}} ha chiesto il controllo", + "grant": "Concessione", + "controlBadge": "In controllo", + "youArePresenting": "Stai presentando in un'altra scheda. Interrompi la presentazione per cedere la parola.", + "created": "Stanza creata", + "invited": "Membri invitati", + "roomTab": "Incontro" + }, + "nav": { + "collab": "Riunioni" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Consenti la condivisione della sessione", + "description": "Consente la condivisione di sessioni live di terminale, RDP, VNC e Telnet a livello di istanza. Quando disabilitata, questa opzione sovrascrive tutte le impostazioni di condivisione per singolo host." + } + }, + "host": { + "allowSessionSharing": { + "label": "Consenti la condivisione della sessione", + "description": "Consenti la condivisione delle sessioni live su questo host tramite link o con altri utenti" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/ja_JP.json b/plugins/session-sharing/locales/translated/ja_JP.json new file mode 100644 index 000000000..b7630383b --- /dev/null +++ b/plugins/session-sharing/locales/translated/ja_JP.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "共有セッションに接続しています...", + "linkInvalid": "この共有リンクは無効、期限切れ、または取り消されています。", + "rateLimited": "試行回数が多すぎます。しばらくしてからもう一度お試しください。", + "sessionEnded": "このセッションは終了しました", + "readOnlyBadge": "閲覧のみ", + "ownerLabel": "ホストオーナー" + }, + "modalTitle": "セッションを共有する", + "shareButton": "共有", + "notReadyToShare": "セッションはまだ共有できません", + "modeTab": { + "link": "リンク", + "user": "ユーザー" + }, + "linkModeDescription": "このリンクを知っている人なら誰でも参加できます。アカウントは必要ありません。", + "userModeDescription": "このホストへのアクセス権を既に持っている特定のユーザーと共有します。まだアクセス権がない場合は、まずホストを共有するか、代わりにリンクを使用してください。共有が完了すると、セッションはユーザーの「接続」タブに表示されます。", + "permissionLevel": { + "label": "権限レベル", + "readOnly": "読み取り専用", + "readOnlyDescription": "セッションをライブで視聴することはできますが、文字入力やインタラクションはできません。", + "readWrite": "読み書き", + "readWriteDescription": "所有者と同様に、セッション内で文字を入力したり操作したりできます。" + }, + "expiryLabel": "リンクの有効期限", + "createLinkButton": "リンクを作成する", + "createShareButton": "ユーザーと共有する", + "searchUsersPlaceholder": "ユーザーを検索...", + "noUsersFound": "ユーザーが見つかりませんでした", + "linkCreated": "共有リンクが作成されました", + "linkCopied": "リンクがクリップボードにコピーされました", + "copyLink": "リンクをコピー", + "shareCreated": "セッションが共有されました。相手の「接続」タブに表示されます。", + "shareFailed": "共有の作成に失敗しました", + "userLacksHostAccess": "そのユーザーはまだこのホストへのアクセス権を持っていません。まずホストを共有するか、代わりにリンクを使用してください。", + "activeShares": "アクティブなシェア", + "noActiveShares": "今セッションで有効な株式はありません", + "revoke": "取り消す", + "revokeConfirmTitle": "この共有を取り消しますか?", + "revokeConfirmDescription": "この共有ファイルを使用している人は、即座にアクセス権を失います。", + "revoked": "シェアが取り消されました", + "revokeFailed": "シェアの取り消しに失敗しました", + "joinCount": "{{count}} 参加する", + "joinCount_other": "{{count}} が参加します", + "expiresAt": "有効期限 {{date}}", + "linkShareBadge": "リンク", + "userShareBadge": "ユーザー: {{username}}", + "loadSharesFailed": "アクティブな共有の読み込みに失敗しました", + "customHoursPlaceholder": "アクセス期限までの時間数" + }, + "collab": { + "createRoom": "新しい部屋", + "roomName": "部屋名", + "persistentRoom": "永続的な部屋", + "persistentRoomHint": "永続的な会議室は、会議終了後もリストに表示され、再利用できます。", + "noRooms": "まだ会議室がありません。会議室を作成して会議を開始してください。", + "openRoom": "開ける", + "endRoom": "会議終了", + "leaveRoom": "離れる", + "deleteMember": "取り除く", + "removeMember": "{{name}} を削除", + "removeMemberTitle": "このメンバーを削除しますか?", + "removeMemberDescription": "{{name}} は直ちにこのルームへのアクセス権を失います。アクティブな SSH ビューアはすべて切断されます。", + "invite": "招待する", + "inviteTitle": "メンバーを招待する", + "members": "メンバー", + "membersPanel": "ルームメンバーと制御リクエスト", + "membersWithCount": "メンバー({{count}})", + "online": "オンライン", + "offline": "オフライン", + "you": "(あなた)", + "hostBadge": "ホスト", + "presenterBadge": "発表します", + "present": "現在", + "presentTitle": "ステージに立つ", + "presentHost": "ホスト", + "searchHosts": "ホストを検索", + "noHostsFound": "利用可能なホストが見つかりませんでした。", + "presentProtocol": "プロトコル", + "stopPresenting": "プレゼンテーションをやめる", + "takeOver": "引き継ぐ", + "takeOverConfirmTitle": "現在のプレゼンテーションを置き換えますか?", + "takeOverDescription": "{{name}} が現在プレゼンテーションを行っています。引き継ぐと、そのステージから全員が切断されます。", + "emptyStage": "発表者はいません。ステージに上がってセッションを共有してください。", + "stageLoading": "ステージに接続中…", + "stageEnded": "プレゼンテーションは終了しました", + "roomEnded": "この会議は終了しました。", + "presenterLabel": "{{name}} がプレゼンテーションを行っています", + "guestLink": "ゲストリンク", + "guestLinkOn": "ゲストリンクが有効になっています。リンクを知っている人なら誰でもステージを視聴できます。", + "guestLinkOff": "ゲストリンクが無効になっています。", + "copyLink": "リンクをコピー", + "rotateLink": "リンクを回転させる", + "disableLink": "リンクを無効にする", + "rotateLinkConfirmTitle": "ゲストリンクを回転させますか?", + "rotateLinkDescription": "現在の接続は直ちに停止します。接続中のSSHゲストは切断されます。", + "disableLinkConfirmTitle": "ゲストリンクを無効にしますか?", + "disableLinkDescription": "匿名SSHビューアは直ちに切断され、現在の接続は機能しなくなります。", + "linkCopied": "リンクをコピーしました", + "roles": "役割", + "users": "ユーザー", + "searchUsers": "ユーザーを検索", + "endConfirmTitle": "この会議を終了しますか?", + "endDescription": "{{name}} は全員に対して終了し、再開することはできません。", + "endPersistentDescription": "{{name}} で現在行われているプレゼンテーションは終了します。この部屋は引き続きご利用いただけます。", + "deleteRoom": "ルームを削除", + "deleteConfirmTitle": "この部屋を削除しますか?", + "deleteDescription": "{{name}} は全員に対して完全に削除され、元に戻すことはできません。", + "invitedTo": "あなたは「{{name}}」に招待されました。", + "guest": { + "title": "ミーティング", + "waiting": "プレゼンターをお待ちしています…", + "linkInvalid": "このゲストリンクは無効であるか、ミーティングが終了しています" + }, + "reopenFromPanel": "この会議タブの有効期限が切れました。会議パネルから会議室を再度開いてください。", + "requestControl": "リクエスト制御", + "cancelControlRequest": "キャンセルリクエスト", + "controlQueue": "制御要求", + "noControlRequests": "保留中のリクエストはありません。", + "grantControlTo": "{{name}}に制御権を与える", + "dismissControlRequest": "{{name}}の制御要求を却下します", + "releaseControl": "リリース制御", + "grantControl": "コントロールを与える", + "revokeControl": "主導権を取り戻す", + "controlRequestedBy": "{{name}} が制御を要求しました", + "grant": "付与", + "controlBadge": "制御する", + "youArePresenting": "あなたは別のタブでプレゼンテーションを行っています。プレゼンテーションを中断して、ステージを譲ってください。", + "created": "ルームが作成されました", + "invited": "会員の皆様を招待いたします", + "roomTab": "ミーティング" + }, + "nav": { + "collab": "会議" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "セッション共有を許可する", + "description": "ライブターミナル、RDP、VNC、およびTelnetセッションをインスタンス全体で共有できるようにします。無効にすると、ホストごとの共有設定がすべて上書きされます。" + } + }, + "host": { + "allowSessionSharing": { + "label": "セッション共有を許可する", + "description": "このホスト上のライブセッションをリンクまたは他のユーザーと共有できるようにします。" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/ko_KR.json b/plugins/session-sharing/locales/translated/ko_KR.json new file mode 100644 index 000000000..619883609 --- /dev/null +++ b/plugins/session-sharing/locales/translated/ko_KR.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "공유 세션에 연결 중...", + "linkInvalid": "이 공유 링크는 유효하지 않거나, 만료되었거나, 취소되었습니다.", + "rateLimited": "시도 횟수가 너무 많습니다. 잠시 후 다시 시도해 주세요.", + "sessionEnded": "이번 세션이 종료되었습니다.", + "readOnlyBadge": "보기 전용", + "ownerLabel": "호스트 소유자" + }, + "modalTitle": "공유 세션", + "shareButton": "공유하다", + "notReadyToShare": "세션을 공유할 준비가 아직 되지 않았습니다.", + "modeTab": { + "link": "링크", + "user": "사용자" + }, + "linkModeDescription": "이 링크만 있으면 누구나 가입할 수 있으며, 계정은 필요하지 않습니다.", + "userModeDescription": "이미 해당 호스트에 접근 권한이 있는 특정 사용자와 공유하세요. 접근 권한이 없는 경우, 먼저 호스트를 공유하거나 링크를 사용하세요. 공유가 완료되면 해당 세션이 사용자의 '연결' 탭에 나타납니다.", + "permissionLevel": { + "label": "권한 수준", + "readOnly": "읽기 전용", + "readOnlyDescription": "실시간으로 세션을 시청할 수는 있지만, 입력하거나 상호 작용할 수는 없습니다.", + "readWrite": "읽기-쓰기", + "readWriteDescription": "소유자와 마찬가지로 세션에 입력하고 상호 작용할 수 있습니다." + }, + "expiryLabel": "링크 만료", + "createLinkButton": "링크 생성", + "createShareButton": "사용자와 공유", + "searchUsersPlaceholder": "사용자 검색...", + "noUsersFound": "사용자를 찾을 수 없습니다.", + "linkCreated": "공유 링크가 생성되었습니다", + "linkCopied": "링크가 클립보드에 복사되었습니다", + "copyLink": "링크 복사", + "shareCreated": "세션이 공유되었습니다. 상대방의 연결 탭에 표시됩니다.", + "shareFailed": "공유 폴더 생성에 실패했습니다.", + "userLacksHostAccess": "해당 사용자는 아직 이 호스트에 대한 접근 권한이 없습니다. 먼저 호스트를 공유하거나 링크를 사용하세요.", + "activeShares": "활성 주식", + "noActiveShares": "이번 세션에는 활성 주식이 없습니다.", + "revoke": "취소", + "revokeConfirmTitle": "이 지분을 취소하시겠습니까?", + "revokeConfirmDescription": "이 공유 폴더를 사용하는 사람은 누구든 즉시 접근 권한을 잃게 됩니다.", + "revoked": "공유 취소됨", + "revokeFailed": "공유 취소에 실패했습니다", + "joinCount": "{{count}} 참여", + "joinCount_other": "{{count}} 참여", + "expiresAt": "만료일 {{date}}", + "linkShareBadge": "링크", + "userShareBadge": "사용자: {{username}}", + "loadSharesFailed": "활성 공유 항목을 로드하는 데 실패했습니다.", + "customHoursPlaceholder": "액세스 만료까지 시간" + }, + "collab": { + "createRoom": "새로운 방", + "roomName": "객실 이름", + "persistentRoom": "지속적인 방", + "persistentRoomHint": "회의가 끝난 후에도 지정된 회의실 목록에 남아 재사용할 수 있습니다.", + "noRooms": "아직 회의실이 없습니다. 회의를 시작하려면 회의실을 만드세요.", + "openRoom": "열려 있는", + "endRoom": "회의 종료", + "leaveRoom": "떠나다", + "deleteMember": "제거하다", + "removeMember": "{{name}}를 제거하세요", + "removeMemberTitle": "이 회원을 삭제하시겠습니까?", + "removeMemberDescription": "{{name}} 사용자는 즉시 이 방에 대한 접근 권한을 잃게 됩니다. 활성화된 SSH 뷰어는 모두 연결이 끊어집니다.", + "invite": "초대하다", + "inviteTitle": "회원 초대", + "members": "회원들", + "membersPanel": "회의실 구성원 및 제어 요청", + "membersWithCount": "멤버({{count}})", + "online": "온라인", + "offline": "오프라인", + "you": "(너)", + "hostBadge": "주인", + "presenterBadge": "소개합니다", + "present": "현재의", + "presentTitle": "무대에 오르세요", + "presentHost": "주인", + "searchHosts": "호스트 검색", + "noHostsFound": "사용 가능한 호스트를 찾을 수 없습니다.", + "presentProtocol": "규약", + "stopPresenting": "프레젠테이션을 중단하세요", + "takeOver": "인수하세요", + "takeOverConfirmTitle": "현재 프레젠테이션을 교체하시겠습니까?", + "takeOverDescription": "{{name}} 님이 현재 발표 중입니다. 발표를 이어받으면 해당 무대에서 모든 사용자의 연결이 끊어집니다.", + "emptyStage": "발표자가 없습니다. 무대에 올라 세션을 공유해 보세요.", + "stageLoading": "무대와 연결 중...", + "stageEnded": "프레젠테이션이 끝났습니다.", + "roomEnded": "이 회의는 종료되었습니다.", + "presenterLabel": "{{name}} 가 발표합니다", + "guestLink": "게스트 링크", + "guestLinkOn": "게스트 링크가 활성화되었습니다. 링크만 있으면 누구나 무대를 시청할 수 있습니다.", + "guestLinkOff": "게스트 링크가 꺼져 있습니다.", + "copyLink": "링크 복사", + "rotateLink": "링크를 회전하세요", + "disableLink": "링크 비활성화", + "rotateLinkConfirmTitle": "게스트 링크를 변경하시겠습니까?", + "rotateLinkDescription": "현재 연결은 즉시 작동을 멈춥니다. 연결된 SSH 게스트는 연결이 끊어집니다.", + "disableLinkConfirmTitle": "게스트 링크를 비활성화하시겠습니까?", + "disableLinkDescription": "익명 SSH 뷰어는 즉시 연결이 끊어지고 현재 링크가 작동을 멈춥니다.", + "linkCopied": "링크 복사됨", + "roles": "역할", + "users": "사용자", + "searchUsers": "검색 사용자", + "endConfirmTitle": "이 회의를 종료하시겠습니까?", + "endDescription": "{{name}} 는 모든 사용자에게 종료되며 다시 열 수 없습니다.", + "endPersistentDescription": "{{name}} 에서 진행 중이던 프레젠테이션이 종료됩니다. 해당 공간은 재사용 가능합니다.", + "deleteRoom": "방 삭제", + "deleteConfirmTitle": "이 방을 삭제하시겠습니까?", + "deleteDescription": "{{name}} 는 모든 사용자에게 영구적으로 삭제되며 되돌릴 수 없습니다.", + "invitedTo": "당신은 \"{{name}} \"에 초대되었습니다.", + "guest": { + "title": "회의", + "waiting": "발표자를 기다리는 중...", + "linkInvalid": "게스트 링크가 유효하지 않거나 회의가 종료되었습니다." + }, + "reopenFromPanel": "이 회의 탭이 만료되었습니다. 회의 패널에서 회의실을 다시 여세요.", + "requestControl": "요청 제어", + "cancelControlRequest": "취소 요청", + "controlQueue": "제어 요청", + "noControlRequests": "보류 중인 요청이 없습니다.", + "grantControlTo": "{{name}}에 제어권을 넘겨주세요", + "dismissControlRequest": "{{name}}의 제어 요청을 기각합니다.", + "releaseControl": "릴리스 컨트롤", + "grantControl": "제어권을 넘겨주세요", + "revokeControl": "주도권을 되찾으세요", + "controlRequestedBy": "{{name}} 제어권을 요청했습니다.", + "grant": "승인하다", + "controlBadge": "통제하에", + "youArePresenting": "다른 탭에서 프레젠테이션을 진행하고 계십니다. 프레젠테이션을 중지하고 다음 사람에게 넘겨주세요.", + "created": "방이 생성되었습니다", + "invited": "회원 초대", + "roomTab": "회의" + }, + "nav": { + "collab": "회의" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "세션 공유 허용", + "description": "라이브 터미널, RDP, VNC 및 Telnet 세션을 인스턴스 전체에서 공유할 수 있도록 허용합니다. 이 기능을 비활성화하면 호스트별 공유 설정이 모두 무시됩니다." + } + }, + "host": { + "allowSessionSharing": { + "label": "세션 공유 허용", + "description": "이 호스트에서 진행되는 라이브 세션을 링크 또는 다른 사용자와 공유하세요." + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/nl_NL.json b/plugins/session-sharing/locales/translated/nl_NL.json new file mode 100644 index 000000000..c39027383 --- /dev/null +++ b/plugins/session-sharing/locales/translated/nl_NL.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Verbinding maken met de gedeelde sessie...", + "linkInvalid": "Deze deelbare link is ongeldig, verlopen of ingetrokken.", + "rateLimited": "Te veel pogingen, probeer het over een tijdje opnieuw.", + "sessionEnded": "Deze sessie is afgelopen.", + "readOnlyBadge": "Alleen bekijken", + "ownerLabel": "Gastheer/gastvrouw" + }, + "modalTitle": "Deel sessie", + "shareButton": "Deel", + "notReadyToShare": "De sessie is nog niet klaar om te delen.", + "modeTab": { + "link": "Link", + "user": "Gebruiker" + }, + "linkModeDescription": "Iedereen met deze link kan deelnemen, een account is niet nodig.", + "userModeDescription": "Deel de host met een specifieke gebruiker die er al toegang toe heeft. Als die gebruiker nog geen toegang heeft, deel de host dan eerst met hem of haar, of gebruik een link. Zodra de sessie is gedeeld, verschijnt deze in het tabblad 'Verbindingen' van de gebruiker.", + "permissionLevel": { + "label": "Toegangsniveau", + "readOnly": "Alleen-lezen", + "readOnlyDescription": "Je kunt de sessie live volgen, maar je kunt niet typen of interageren.", + "readWrite": "Lezen en schrijven", + "readWriteDescription": "Kan typen en interactie hebben met de sessie, net als de eigenaar." + }, + "expiryLabel": "Link verloopt", + "createLinkButton": "Link maken", + "createShareButton": "Delen met gebruiker", + "searchUsersPlaceholder": "Gebruikers doorzoeken...", + "noUsersFound": "Geen gebruikers gevonden", + "linkCreated": "Deellink aangemaakt", + "linkCopied": "Link gekopieerd naar het klembord.", + "copyLink": "Kopieer link", + "shareCreated": "De sessie is gedeeld. Deze verschijnt in hun tabblad 'Verbindingen'.", + "shareFailed": "Aanmaken van de share is mislukt", + "userLacksHostAccess": "Die gebruiker heeft nog geen toegang tot deze host. Deel de host eerst met hem of haar, of gebruik een link.", + "activeShares": "Actieve aandelen", + "noActiveShares": "Geen actieve aandelen voor deze sessie.", + "revoke": "Herroepen", + "revokeConfirmTitle": "Wilt u dit aandeel intrekken?", + "revokeConfirmDescription": "Iedereen die deze gedeelde map gebruikt, verliest onmiddellijk de toegang.", + "revoked": "Aandeel ingetrokken", + "revokeFailed": "Het intrekken van het aandeel is mislukt.", + "joinCount": "{{count}} join", + "joinCount_other": "{{count}} sluit zich aan", + "expiresAt": "Verloopt {{date}}", + "linkShareBadge": "Link", + "userShareBadge": "Gebruiker: {{username}}", + "loadSharesFailed": "Het laden van actieve aandelen is mislukt.", + "customHoursPlaceholder": "Aantal uren tot de toegang verloopt" + }, + "collab": { + "createRoom": "Nieuwe kamer", + "roomName": "Kamernaam", + "persistentRoom": "Permanente kamer", + "persistentRoomHint": "Kamers die permanent gereserveerd blijven, blijven na afloop van de vergadering beschikbaar en kunnen opnieuw worden gebruikt.", + "noRooms": "Er zijn nog geen vergaderruimtes. Maak er een aan om een vergadering te starten.", + "openRoom": "Open", + "endRoom": "Einde vergadering", + "leaveRoom": "Vertrekken", + "deleteMember": "Verwijderen", + "removeMember": "Verwijder {{name}}", + "removeMemberTitle": "Dit lid verwijderen?", + "removeMemberDescription": "{{name}} verliest onmiddellijk de toegang tot deze ruimte. Elke actieve SSH-viewer wordt losgekoppeld.", + "invite": "Uitnodiging", + "inviteTitle": "Nodig leden uit", + "members": "Leden", + "membersPanel": "Kamerleden en beheerverzoeken", + "membersWithCount": "Leden ({{count}})", + "online": "online", + "offline": "offline", + "you": "(Jij)", + "hostBadge": "Gastheer", + "presenterBadge": "Presentatie", + "present": "Cadeau", + "presentTitle": "Betreed het podium", + "presentHost": "Gastheer", + "searchHosts": "Zoek hosts", + "noHostsFound": "Geen beschikbare hosts gevonden.", + "presentProtocol": "Protocol", + "stopPresenting": "Stop met presenteren", + "takeOver": "Neem het over", + "takeOverConfirmTitle": "De huidige presentatie vervangen?", + "takeOverDescription": "{{name}} presenteert nu. Als je het overneemt, wordt iedereen van dat podium losgekoppeld.", + "emptyStage": "Niemand geeft een presentatie. Betreed het podium om een sessie te delen.", + "stageLoading": "Verbinding maken met het podium...", + "stageEnded": "De presentatie eindigde.", + "roomEnded": "Deze vergadering is afgelopen.", + "presenterLabel": "{{name}} presenteert", + "guestLink": "Gastlink", + "guestLinkOn": "De gastlink is ingeschakeld. Iedereen met de link kan het optreden bekijken.", + "guestLinkOff": "De gastlink is uitgeschakeld.", + "copyLink": "Kopieer link", + "rotateLink": "Draai de link", + "disableLink": "Link uitschakelen", + "rotateLinkConfirmTitle": "De gastlink roteren?", + "rotateLinkDescription": "De huidige verbinding wordt onmiddellijk verbroken. Verbonden SSH-gasten worden losgekoppeld.", + "disableLinkConfirmTitle": "De gastlink uitschakelen?", + "disableLinkDescription": "Anonieme SSH-gebruikers worden onmiddellijk losgekoppeld en de huidige verbinding zal niet meer werken.", + "linkCopied": "Link gekopieerd", + "roles": "Rollen", + "users": "Gebruikers", + "searchUsers": "Zoek gebruikers", + "endConfirmTitle": "Deze vergadering beëindigen?", + "endDescription": "{{name}} zal voor iedereen eindigen en kan niet opnieuw worden geopend.", + "endPersistentDescription": "De actieve presentatie in {{name}} wordt gestopt. De ruimte blijft beschikbaar voor hergebruik.", + "deleteRoom": "Verwijder kamer", + "deleteConfirmTitle": "Deze kamer verwijderen?", + "deleteDescription": "{{name}} wordt permanent verwijderd voor iedereen en kan niet ongedaan worden gemaakt.", + "invitedTo": "Je werd uitgenodigd voor \"{{name}}\"", + "guest": { + "title": "Ontmoeting", + "waiting": "Wachten op de presentator...", + "linkInvalid": "Deze gastlink is ongeldig of de vergadering is afgelopen." + }, + "reopenFromPanel": "Dit vergadertabblad is verlopen. Heropen de ruimte via het paneel Vergaderingen.", + "requestControl": "Verzoekbeheer", + "cancelControlRequest": "Verzoek annuleren", + "controlQueue": "Controleverzoeken", + "noControlRequests": "Geen openstaande aanvragen.", + "grantControlTo": "Geef de controle aan {{name}}", + "dismissControlRequest": "Negeer het controleverzoek van {{name}}", + "releaseControl": "Ontgrendeling", + "grantControl": "Geef de controle", + "revokeControl": "Neem de controle terug", + "controlRequestedBy": "{{name}} vroeg om controle", + "grant": "Studiebeurs", + "controlBadge": "Onder controle", + "youArePresenting": "Je presenteert momenteel in een ander tabblad. Stop met presenteren en geef het woord aan iemand anders.", + "created": "Kamer gecreëerd", + "invited": "Leden uitgenodigd", + "roomTab": "Ontmoeting" + }, + "nav": { + "collab": "Vergaderingen" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Sessiedeling toestaan", + "description": "Sta toe dat live terminal-, RDP-, VNC- en Telnet-sessies instantiebreed worden gedeeld. Deze functie overschrijft elke instelling voor delen per host wanneer deze is uitgeschakeld." + } + }, + "host": { + "allowSessionSharing": { + "label": "Sessiedeling toestaan", + "description": "Sta toe dat live sessies op deze host via een link of met andere gebruikers worden gedeeld." + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/no_NO.json b/plugins/session-sharing/locales/translated/no_NO.json new file mode 100644 index 000000000..b6c23a07e --- /dev/null +++ b/plugins/session-sharing/locales/translated/no_NO.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Kobler til delt økt...", + "linkInvalid": "Denne delingslenken er ugyldig, utløpt eller har blitt tilbakekalt", + "rateLimited": "For mange forsøk, prøv igjen snart", + "sessionEnded": "Denne økten er avsluttet", + "readOnlyBadge": "Kun visning", + "ownerLabel": "Vertseier" + }, + "modalTitle": "Del økt", + "shareButton": "Dele", + "notReadyToShare": "Økten er ikke klar til deling ennå", + "modeTab": { + "link": "Lenke", + "user": "Bruker" + }, + "linkModeDescription": "Alle med denne lenken kan bli med, ingen konto kreves.", + "userModeDescription": "Del med en bestemt bruker som allerede har tilgang til denne verten. Hvis de ikke har tilgang ennå, del verten med dem først eller bruk en lenke i stedet. Når økten er delt, vises den i deres Tilkoblinger-fane.", + "permissionLevel": { + "label": "Tillatelsesnivå", + "readOnly": "Skrivebeskyttet", + "readOnlyDescription": "Kan se økten direkte, men kan ikke skrive eller samhandle.", + "readWrite": "Lese-skrive", + "readWriteDescription": "Kan skrive og samhandle med økten akkurat som eieren." + }, + "expiryLabel": "Utløpsdato for lenke", + "createLinkButton": "Opprett lenke", + "createShareButton": "Del med brukeren", + "searchUsersPlaceholder": "Søk etter brukere...", + "noUsersFound": "Ingen brukere funnet", + "linkCreated": "Delingslenke opprettet", + "linkCopied": "Lenke kopiert til utklippstavlen", + "copyLink": "Kopier lenke", + "shareCreated": "Økten er delt. Den vil vises i Tilkoblinger-fanen deres.", + "shareFailed": "Kunne ikke opprette delingen", + "userLacksHostAccess": "Den brukeren har ikke tilgang til denne verten ennå. Del verten med dem først, eller bruk en lenke i stedet.", + "activeShares": "Aktive aksjer", + "noActiveShares": "Ingen aktive delinger for denne økten", + "revoke": "Tilbakekalling", + "revokeConfirmTitle": "Tilbakekalle denne delingen?", + "revokeConfirmDescription": "Alle som bruker denne delingen vil miste tilgangen umiddelbart.", + "revoked": "Deling tilbakekalt", + "revokeFailed": "Kunne ikke tilbakekalle delingen", + "joinCount": "{{count}} bli med", + "joinCount_other": "{{count}} blir med", + "expiresAt": "Utløper {{date}}", + "linkShareBadge": "Lenke", + "userShareBadge": "Bruker: {{username}}", + "loadSharesFailed": "Kunne ikke laste inn aktive delinger", + "customHoursPlaceholder": "Timer til tilgangen utløper" + }, + "collab": { + "createRoom": "Nytt rom", + "roomName": "Romnavn", + "persistentRoom": "Permanent rom", + "persistentRoomHint": "Permanente rom forblir oppført etter at møtet er avsluttet og kan brukes om igjen.", + "noRooms": "Ingen rom ennå. Opprett et for å starte et møte.", + "openRoom": "Åpne", + "endRoom": "Avslutt møtet", + "leaveRoom": "Forlate", + "deleteMember": "Fjerne", + "removeMember": "Fjern {{name}}", + "removeMemberTitle": "Fjerne dette medlemmet?", + "removeMemberDescription": "{{name}} mister tilgangen til dette rommet umiddelbart. Alle aktive SSH-lesere blir frakoblet.", + "invite": "Invitere", + "inviteTitle": "Inviter medlemmer", + "members": "Medlemmer", + "membersPanel": "Rommedlemmer og kontrollforespørsler", + "membersWithCount": "Medlemmer ({{count}})", + "online": "på nett", + "offline": "frakoblet", + "you": "(du)", + "hostBadge": "Vert", + "presenterBadge": "Presenterer", + "present": "Nåværende", + "presentTitle": "Innta scenen", + "presentHost": "Vert", + "searchHosts": "Søk etter verter", + "noHostsFound": "Ingen tilgjengelige verter funnet.", + "presentProtocol": "Protokoll", + "stopPresenting": "Slutt å presentere", + "takeOver": "Ta over", + "takeOverConfirmTitle": "Erstatte den nåværende presentasjonen?", + "takeOverDescription": "{{name}} presenterer nå. Å ta over vil koble alle fra det stadiet.", + "emptyStage": "Ingen presenterer. Gå på scenen for å dele en økt.", + "stageLoading": "Kobler til scenen...", + "stageEnded": "Presentasjonen ble avsluttet", + "roomEnded": "Dette møtet er avsluttet", + "presenterLabel": "{{name}} presenterer", + "guestLink": "Gjestelenke", + "guestLinkOn": "Gjestelenken er på. Alle med lenken kan se scenen.", + "guestLinkOff": "Gjestelenken er av.", + "copyLink": "Kopier lenke", + "rotateLink": "Roter lenken", + "disableLink": "Deaktiver lenken", + "rotateLinkConfirmTitle": "Roter gjestelenken?", + "rotateLinkDescription": "Den nåværende lenken vil slutte å virke umiddelbart. Tilkoblede SSH-gjester vil bli frakoblet.", + "disableLinkConfirmTitle": "Deaktivere gjestelenken?", + "disableLinkDescription": "Anonyme SSH-lesere vil bli frakoblet umiddelbart, og den nåværende koblingen vil slutte å fungere.", + "linkCopied": "Lenken er kopiert", + "roles": "Roller", + "users": "Brukere", + "searchUsers": "Søk etter brukere", + "endConfirmTitle": "Avslutte dette møtet?", + "endDescription": "{{name}} vil avsluttes for alle og kan ikke åpnes igjen.", + "endPersistentDescription": "Den aktive presentasjonen i {{name}} stopper. Rommet er fortsatt tilgjengelig for gjenbruk.", + "deleteRoom": "Slett rom", + "deleteConfirmTitle": "Slette dette rommet?", + "deleteDescription": "{{name}} vil bli permanent slettet for alle og kan ikke angres.", + "invitedTo": "Du ble invitert til «{{name}}»", + "guest": { + "title": "Møte", + "waiting": "Venter på programlederen...", + "linkInvalid": "Denne gjestelenken er ugyldig, eller møtet er avsluttet" + }, + "reopenFromPanel": "Denne møtefanen er utløpt. Åpne rommet på nytt fra Møter-panelet.", + "requestControl": "Be om kontroll", + "cancelControlRequest": "Avbryt forespørsel", + "controlQueue": "Kontrollforespørsler", + "noControlRequests": "Ingen ventende forespørsler.", + "grantControlTo": "Gi kontroll til {{name}}", + "dismissControlRequest": "Avvis kontrollforespørselen til {{name}}", + "releaseControl": "Slipp kontrollen", + "grantControl": "Gi kontroll", + "revokeControl": "Ta tilbake kontrollen", + "controlRequestedBy": "{{name}} ba om kontroll", + "grant": "Stipend", + "controlBadge": "I kontroll", + "youArePresenting": "Du presenterer i en annen fane. Stopp presentasjonen for å gi fra deg scenen.", + "created": "Rommet er opprettet", + "invited": "Medlemmer invitert", + "roomTab": "Møte" + }, + "nav": { + "collab": "Møter" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Tillat øktdeling", + "description": "Tillat deling av Live Terminal-, RDP-, VNC- og Telnet-økter på tvers av hele instansen. Overstyrer alle delingsalternativer per vert når de er deaktivert." + } + }, + "host": { + "allowSessionSharing": { + "label": "Tillat øktdeling", + "description": "La direktesendte økter på denne verten deles via lenke eller med andre brukere" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/pl_PL.json b/plugins/session-sharing/locales/translated/pl_PL.json new file mode 100644 index 000000000..479fc5d06 --- /dev/null +++ b/plugins/session-sharing/locales/translated/pl_PL.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Łączenie z sesją współdzieloną...", + "linkInvalid": "Ten link do udostępniania jest nieprawidłowy, wygasł lub został odwołany", + "rateLimited": "Zbyt wiele prób, spróbuj ponownie za chwilę", + "sessionEnded": "Ta sesja dobiegła końca", + "readOnlyBadge": "Tylko do podglądu", + "ownerLabel": "Właściciel gospodarza" + }, + "modalTitle": "Udostępnij sesję", + "shareButton": "Udostępnij", + "notReadyToShare": "Sesja nie jest jeszcze gotowa do udostępnienia", + "modeTab": { + "link": "Link", + "user": "Użytkownik" + }, + "linkModeDescription": "Każdy mający ten link może dołączyć, nie jest wymagane założenie konta.", + "userModeDescription": "Udostępnij konkretnemu użytkownikowi, który ma już dostęp do tego hosta. Jeśli jeszcze go nie ma, najpierw udostępnij mu hosta albo skorzystaj z linku. Po udostępnieniu sesja pojawi się na jego karcie Połączenia.", + "permissionLevel": { + "label": "Poziom uprawnień", + "readOnly": "Tylko do odczytu", + "readOnlyDescription": "Może oglądać sesję na żywo, ale nie może pisać ani nic w niej zmieniać.", + "readWrite": "Odczyt-zapis", + "readWriteDescription": "Może pisać i uczestniczyć w sesji tak samo jak właściciel." + }, + "expiryLabel": "Wygaśnięcie linku", + "createLinkButton": "Utwórz link", + "createShareButton": "Udostępnij użytkownikowi", + "searchUsersPlaceholder": "Wyszukaj użytkowników...", + "noUsersFound": "Nie znaleziono użytkowników", + "linkCreated": "Utworzono link do udostępniania", + "linkCopied": "Link skopiowany do schowka", + "copyLink": "Kopiuj link", + "shareCreated": "Udostępniono sesję. Pojawi się na jego karcie Połączenia.", + "shareFailed": "Nie udało się utworzyć udostępnienia", + "userLacksHostAccess": "Ten użytkownik nie ma jeszcze dostępu do tego hosta. Udostępnij mu najpierw hosta lub skorzystaj z linku.", + "activeShares": "Aktywne udostępnienia", + "noActiveShares": "Brak aktywnych udostępnień tej sesji", + "revoke": "Unieważnij", + "revokeConfirmTitle": "Cofnąć to udostępnienie?", + "revokeConfirmDescription": "Każdy, kto korzysta z tego udostępnienia, natychmiast utraci dostęp.", + "revoked": "Unieważniono udostępnienie", + "revokeFailed": "Nie udało się unieważnić udostępnienia", + "joinCount": "{{count}} dołączenie", + "joinCount_other": "{{count}} dołączeń", + "expiresAt": "Wygasa {{date}}", + "linkShareBadge": "Link", + "userShareBadge": "Użytkownik: {{username}}", + "loadSharesFailed": "Nie udało się załadować aktywnych udostępnień", + "customHoursPlaceholder": "Godziny do wygaśnięcia dostępu" + }, + "collab": { + "createRoom": "Nowy pokój", + "roomName": "Nazwa pokoju", + "persistentRoom": "Trwały pokój", + "persistentRoomHint": "Trwałe pokoje pozostają na liście po zakończeniu spotkania i można je ponownie wykorzystać.", + "noRooms": "Brak pokoi. Utwórz jeden, aby rozpocząć spotkanie.", + "openRoom": "Otwarte", + "endRoom": "Zakończ spotkanie", + "leaveRoom": "Wyjechać", + "deleteMember": "Usunąć", + "removeMember": "Usuń {{name}}", + "removeMemberTitle": "Usunąć tego członka?", + "removeMemberDescription": "{{name}} natychmiast utraci dostęp do tego pokoju. Każdy aktywny użytkownik SSH zostanie rozłączony.", + "invite": "Zapraszać", + "inviteTitle": "Zaproś członków", + "members": "Członkowie", + "membersPanel": "Członkowie pokoju i prośby o kontrolę", + "membersWithCount": "Członkowie ({{count}})", + "online": "w sieci", + "offline": "nieaktywny", + "you": "(Ty)", + "hostBadge": "Gospodarz", + "presenterBadge": "Przedstawianie", + "present": "Obecny", + "presentTitle": "Wejdź na scenę", + "presentHost": "Gospodarz", + "searchHosts": "Wyszukaj gospodarzy", + "noHostsFound": "Nie znaleziono dostępnych hostów.", + "presentProtocol": "Protokół", + "stopPresenting": "Zatrzymaj prezentację", + "takeOver": "Przejąć", + "takeOverConfirmTitle": "Zastąpić bieżącą prezentację?", + "takeOverDescription": "{{name}} jest teraz prezentowany. Przejęcie kontroli spowoduje odłączenie wszystkich od tej sceny.", + "emptyStage": "Nikt nie prowadzi prezentacji. Wejdź na scenę i podziel się swoją sesją.", + "stageLoading": "Łączę się ze sceną...", + "stageEnded": "Prezentacja dobiegła końca", + "roomEnded": "To spotkanie dobiegło końca", + "presenterLabel": "{{name}} prezentuje", + "guestLink": "Link gościnny", + "guestLinkOn": "Link dla gości jest włączony. Każdy, kto ma link, może obejrzeć scenę.", + "guestLinkOff": "Link dla gości jest wyłączony.", + "copyLink": "Kopiuj link", + "rotateLink": "Obróć link", + "disableLink": "Wyłącz łącze", + "rotateLinkConfirmTitle": "Obrócić link gościa?", + "rotateLinkDescription": "Obecny link natychmiast przestanie działać. Połączeni goście SSH zostaną rozłączeni.", + "disableLinkConfirmTitle": "Wyłączyć link gościa?", + "disableLinkDescription": "Anonimowi użytkownicy SSH zostaną natychmiast rozłączeni, a bieżące łącze przestanie działać.", + "linkCopied": "Link skopiowany", + "roles": "Role", + "users": "Użytkownicy", + "searchUsers": "Wyszukaj użytkowników", + "endConfirmTitle": "Zakończyć to spotkanie?", + "endDescription": "{{name}} zakończy się dla wszystkich i nie będzie można go ponownie otworzyć.", + "endPersistentDescription": "Aktywna prezentacja w {{name}} zostanie zatrzymana. Pokój pozostaje dostępny do ponownego użycia.", + "deleteRoom": "Usuń pokój", + "deleteConfirmTitle": "Usunąć ten pokój?", + "deleteDescription": "{{name}} zostanie trwale usunięty u wszystkich i nie będzie można tego cofnąć.", + "invitedTo": "Zostałeś zaproszony do „{{name}}”", + "guest": { + "title": "Spotkanie", + "waiting": "Czekam na prezentera...", + "linkInvalid": "Ten link dla gościa jest nieprawidłowy lub spotkanie się zakończyło" + }, + "reopenFromPanel": "Ta karta spotkania wygasła. Otwórz ponownie pokój z panelu Spotkania.", + "requestControl": "Żądanie kontroli", + "cancelControlRequest": "Anuluj prośbę", + "controlQueue": "Żądania kontrolne", + "noControlRequests": "Brak oczekujących żądań.", + "grantControlTo": "Przekaż kontrolę {{name}}", + "dismissControlRequest": "Odrzuć żądanie kontroli {{name}}", + "releaseControl": "Kontrola wydania", + "grantControl": "Oddaj kontrolę", + "revokeControl": "Odzyskaj kontrolę", + "controlRequestedBy": "{{name}} poprosił o kontrolę", + "grant": "Dotacja", + "controlBadge": "Pod kontrolą", + "youArePresenting": "Prezentujesz na innej karcie. Zatrzymaj prezentację, aby przekazać scenę.", + "created": "Pokój utworzony", + "invited": "Zaproszeni członkowie", + "roomTab": "Spotkanie" + }, + "nav": { + "collab": "Spotkania" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Zezwól na udostępnianie sesji", + "description": "Zezwalaj na udostępnianie sesji terminala na żywo, RDP, VNC i Telnet w obrębie instancji. Po wyłączeniu zastępuje wszystkie przełączniki udostępniania dla poszczególnych hostów." + } + }, + "host": { + "allowSessionSharing": { + "label": "Zezwól na udostępnianie sesji", + "description": "Zezwól na udostępnianie sesji na żywo na tym hoście przez link lub innym użytkownikom" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/pt_BR.json b/plugins/session-sharing/locales/translated/pt_BR.json new file mode 100644 index 000000000..3c1d56878 --- /dev/null +++ b/plugins/session-sharing/locales/translated/pt_BR.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Conectando à sessão compartilhada...", + "linkInvalid": "Este link de compartilhamento é inválido, expirou ou foi revogado.", + "rateLimited": "Muitas tentativas foram feitas, tente novamente em breve.", + "sessionEnded": "Esta sessão terminou.", + "readOnlyBadge": "Somente visualização", + "ownerLabel": "Proprietário do anfitrião" + }, + "modalTitle": "Sessão de compartilhamento", + "shareButton": "Compartilhar", + "notReadyToShare": "A sessão ainda não está pronta para ser compartilhada.", + "modeTab": { + "link": "Link", + "user": "Usuário" + }, + "linkModeDescription": "Qualquer pessoa com este link pode participar, sem necessidade de criar uma conta.", + "userModeDescription": "Compartilhe com um usuário específico que já tenha acesso a este host. Caso ele ainda não tenha acesso, compartilhe o host com ele primeiro ou use um link. Após o compartilhamento, a sessão aparecerá na aba Conexões do usuário.", + "permissionLevel": { + "label": "Nível de permissão", + "readOnly": "Somente leitura", + "readOnlyDescription": "É possível assistir à sessão ao vivo, mas não é possível digitar ou interagir.", + "readWrite": "Ler e escrever", + "readWriteDescription": "Pode digitar e interagir com a sessão da mesma forma que o proprietário." + }, + "expiryLabel": "Expiração do link", + "createLinkButton": "Criar link", + "createShareButton": "Compartilhar com o usuário", + "searchUsersPlaceholder": "Pesquisar usuários...", + "noUsersFound": "Nenhum usuário encontrado", + "linkCreated": "Compartilhar link criado", + "linkCopied": "Link copiado para a área de transferência", + "copyLink": "Copiar link", + "shareCreated": "Sessão compartilhada. Ela aparecerá na aba Conexões deles.", + "shareFailed": "Falha ao criar compartilhamento", + "userLacksHostAccess": "Esse usuário ainda não tem acesso a este host. Compartilhe o host com ele primeiro ou use um link.", + "activeShares": "Ações ativas", + "noActiveShares": "Nenhuma ação ativa nesta sessão.", + "revoke": "Revogar", + "revokeConfirmTitle": "Revogar essa participação?", + "revokeConfirmDescription": "Qualquer pessoa que usar este compartilhamento perderá o acesso imediatamente.", + "revoked": "Ações revogadas", + "revokeFailed": "Falha ao revogar a participação", + "joinCount": "{{count}} junte-se", + "joinCount_other": "{{count}} entra", + "expiresAt": "Expira em {{date}}", + "linkShareBadge": "Link", + "userShareBadge": "Usuário: {{username}}", + "loadSharesFailed": "Falha ao carregar compartilhamentos ativos", + "customHoursPlaceholder": "Horas restantes até o acesso expirar" + }, + "collab": { + "createRoom": "Quarto novo", + "roomName": "Nome do quarto", + "persistentRoom": "Sala persistente", + "persistentRoomHint": "As salas permanentes permanecem listadas após o término da reunião e podem ser reutilizadas.", + "noRooms": "Ainda não há salas. Crie uma para iniciar uma reunião.", + "openRoom": "Abrir", + "endRoom": "Encerramento da reunião", + "leaveRoom": "Deixar", + "deleteMember": "Remover", + "removeMember": "Remover {{name}}", + "removeMemberTitle": "Remover este membro?", + "removeMemberDescription": "{{name}} perderá o acesso a esta sala imediatamente. Qualquer visualizador SSH ativo será desconectado.", + "invite": "Convidar", + "inviteTitle": "Convide membros", + "members": "Membros", + "membersPanel": "Membros da sala e solicitações de controle", + "membersWithCount": "Membros ({{count}})", + "online": "on-line", + "offline": "offline", + "you": "(você)", + "hostBadge": "Hospedar", + "presenterBadge": "Apresentando", + "present": "Presente", + "presentTitle": "Suba ao palco", + "presentHost": "Hospedar", + "searchHosts": "Pesquisar anfitriões", + "noHostsFound": "Nenhum host disponível encontrado.", + "presentProtocol": "Protocolo", + "stopPresenting": "Pare de apresentar", + "takeOver": "Assumir", + "takeOverConfirmTitle": "Substituir a apresentação atual?", + "takeOverDescription": "{{name}} está apresentando agora. Assumir o controle desconectará todos desse palco.", + "emptyStage": "Ninguém está apresentando. Suba ao palco para compartilhar uma sessão.", + "stageLoading": "Conectando-se ao palco...", + "stageEnded": "A apresentação terminou.", + "roomEnded": "Esta reunião terminou.", + "presenterLabel": "{{name}} está apresentando", + "guestLink": "Link para convidado", + "guestLinkOn": "O link para convidados está ativado. Qualquer pessoa com o link pode assistir à apresentação.", + "guestLinkOff": "O link para visitantes está desativado.", + "copyLink": "Copiar link", + "rotateLink": "Rotacionar link", + "disableLink": "Desativar link", + "rotateLinkConfirmTitle": "Girar o link do convidado?", + "rotateLinkDescription": "A conexão atual deixará de funcionar imediatamente. Os dispositivos convidados SSH conectados serão desconectados.", + "disableLinkConfirmTitle": "Desativar o link de convidado?", + "disableLinkDescription": "Os visualizadores SSH anônimos serão desconectados imediatamente e a conexão atual deixará de funcionar.", + "linkCopied": "Link copiado", + "roles": "Funções", + "users": "Usuários", + "searchUsers": "Usuários da pesquisa", + "endConfirmTitle": "Encerrar esta reunião?", + "endDescription": "{{name}} será encerrado para todos e não poderá ser reaberto.", + "endPersistentDescription": "A apresentação ativa em {{name}} será interrompida. A sala permanece disponível para reutilização.", + "deleteRoom": "Excluir sala", + "deleteConfirmTitle": "Apagar esta sala?", + "deleteDescription": "{{name}} será excluído permanentemente para todos e não poderá ser desfeito.", + "invitedTo": "Você foi convidado para \"{{name}}\"", + "guest": { + "title": "Reunião", + "waiting": "Aguardando o apresentador...", + "linkInvalid": "Este link de convidado é inválido ou a reunião já terminou." + }, + "reopenFromPanel": "Esta aba da reunião expirou. Reabra a sala no painel Reuniões.", + "requestControl": "Controle de solicitação", + "cancelControlRequest": "Cancelar solicitação", + "controlQueue": "Solicitações de controle", + "noControlRequests": "Não há solicitações pendentes.", + "grantControlTo": "Dê o controle para {{name}}", + "dismissControlRequest": "Rejeitar solicitação de controle de {{name}}", + "releaseControl": "Liberar controle", + "grantControl": "Dê o controle", + "revokeControl": "Retome o controle", + "controlRequestedBy": "{{name}} solicitou o controle", + "grant": "Conceder", + "controlBadge": "No controle", + "youArePresenting": "Você está apresentando em outra aba. Pare de apresentar para passar a palavra.", + "created": "Quarto criado", + "invited": "Membros convidados", + "roomTab": "Reunião" + }, + "nav": { + "collab": "Reuniões" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Permitir compartilhamento de sessão", + "description": "Permite que sessões de terminal ao vivo, RDP, VNC e Telnet sejam compartilhadas em toda a instância. Substitui todas as configurações de compartilhamento por host quando desativada." + } + }, + "host": { + "allowSessionSharing": { + "label": "Permitir compartilhamento de sessão", + "description": "Permitir que as sessões ao vivo neste servidor sejam compartilhadas por meio de link ou com outros usuários." + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/pt_PT.json b/plugins/session-sharing/locales/translated/pt_PT.json new file mode 100644 index 000000000..9258aa6c4 --- /dev/null +++ b/plugins/session-sharing/locales/translated/pt_PT.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Ligar à sessão partilhada...", + "linkInvalid": "Este link de partilha é inválido, expirou ou foi revogado.", + "rateLimited": "Muitas tentativas foram feitas, tente novamente em breve.", + "sessionEnded": "Esta sessão terminou.", + "readOnlyBadge": "Somente visualização", + "ownerLabel": "Proprietário do anfitrião" + }, + "modalTitle": "Sessão de partilha", + "shareButton": "Partilhar", + "notReadyToShare": "A sessão ainda não está pronta para ser partilhada.", + "modeTab": { + "link": "Ligação", + "user": "Usuário" + }, + "linkModeDescription": "Qualquer pessoa com este link pode participar, sem necessidade de criar conta.", + "userModeDescription": "Partilhe com um utilizador específico que já tenha acesso a este host. Caso ainda não tenha acesso, partilhe primeiro o host com ele ou utilize um link. Após a partilha, a sessão aparecerá no separador Conexões do utilizador.", + "permissionLevel": { + "label": "Nível de permissão", + "readOnly": "Somente leitura", + "readOnlyDescription": "É possível assistir à sessão em direto, mas não é possível digitar ou interagir.", + "readWrite": "Ler e escrever", + "readWriteDescription": "Pode digitar e interagir com a sessão da mesma forma que o proprietário." + }, + "expiryLabel": "Expiração do link", + "createLinkButton": "Criar link", + "createShareButton": "Partilhar com o usuário", + "searchUsersPlaceholder": "Pesquisar utilizadores...", + "noUsersFound": "Nenhum utilizador encontrado", + "linkCreated": "Partilhar link criado", + "linkCopied": "Ligação copiada para a área de transferência", + "copyLink": "Copiar link", + "shareCreated": "Sessão partilhada. Ela aparecerá no separador Conexões deles.", + "shareFailed": "Falha ao criar partilha", + "userLacksHostAccess": "Esse utilizador ainda não tem acesso a este host. Partilhe o host com ele primeiro ou use um link.", + "activeShares": "Ações ativas", + "noActiveShares": "Sem ação ativa nesta sessão.", + "revoke": "Revogar", + "revokeConfirmTitle": "Revogar essa participação?", + "revokeConfirmDescription": "Qualquer pessoa que utilize esta partilha perderá o acesso imediatamente.", + "revoked": "Ações revogadas", + "revokeFailed": "Falha ao revogar a participação", + "joinCount": "{{count}} junte-se", + "joinCount_other": "{{count}} entra", + "expiresAt": "Expira em {{date}}", + "linkShareBadge": "Ligação", + "userShareBadge": "Utilizador: {{username}}", + "loadSharesFailed": "Falha ao carregar partilhas ativas", + "customHoursPlaceholder": "Horas até à expiração do acesso" + }, + "collab": { + "createRoom": "Quarto novo", + "roomName": "Nome do quarto", + "persistentRoom": "Sala persistente", + "persistentRoomHint": "As salas permanentes permanecem listadas após o término da reunião e podem ser reutilizadas.", + "noRooms": "Ainda não há salas. Crie uma para iniciar uma reunião.", + "openRoom": "Aberto", + "endRoom": "Encerramento da reunião", + "leaveRoom": "Ausência", + "deleteMember": "Remover", + "removeMember": "Remover {{name}}", + "removeMemberTitle": "Remover este membro?", + "removeMemberDescription": "{{name}} perderá o acesso a esta sala imediatamente. Qualquer visualizador SSH ativo será desligado.", + "invite": "Convidar", + "inviteTitle": "Convide os membros", + "members": "Membros", + "membersPanel": "Membros da sala e pedidos de controlo", + "membersWithCount": "Membros ({{count}})", + "online": "online", + "offline": "offline", + "you": "(tu)", + "hostBadge": "Anfitrião", + "presenterBadge": "Apresentando", + "present": "Presente", + "presentTitle": "Suba ao palco", + "presentHost": "Anfitrião", + "searchHosts": "Pesquisar anfitriões", + "noHostsFound": "Nenhum host disponível encontrado.", + "presentProtocol": "Protocolo", + "stopPresenting": "Pare de apresentar", + "takeOver": "Assumir", + "takeOverConfirmTitle": "Substituir a apresentação atual?", + "takeOverDescription": "{{name}} está a apresentar agora. Assumir o controlo desconectará todos desse palco.", + "emptyStage": "Ninguém está a apresentar. Suba ao palco para partilhar uma sessão.", + "stageLoading": "Ligar-se ao palco...", + "stageEnded": "A apresentação terminou.", + "roomEnded": "Esta reunião terminou.", + "presenterLabel": "{{name}} está a apresentar", + "guestLink": "Link para convidado", + "guestLinkOn": "O link para convidados está ativado. Qualquer pessoa com o link pode assistir à apresentação.", + "guestLinkOff": "O link para os visitantes está desativado.", + "copyLink": "Copiar link", + "rotateLink": "Rotacionar ligação", + "disableLink": "Desativar ligação", + "rotateLinkConfirmTitle": "Rodar o link do convidado?", + "rotateLinkDescription": "A ligação atual deixará de funcionar imediatamente. Os dispositivos convidados SSH ligados serão desligados.", + "disableLinkConfirmTitle": "Desativar o link de convidado?", + "disableLinkDescription": "Os visualizadores SSH anónimos serão imediatamente desligados e a ligação atual deixará de funcionar.", + "linkCopied": "Link copiado", + "roles": "Funções", + "users": "Usuários", + "searchUsers": "Os utilizadores da pesquisa", + "endConfirmTitle": "Encerrar esta reunião?", + "endDescription": "{{name}} será encerrado para todos e não poderá ser reaberto.", + "endPersistentDescription": "A apresentação ativa em {{name}} será interrompida. A sala continua disponível para reutilização.", + "deleteRoom": "Apagar sala", + "deleteConfirmTitle": "Apagar esta sala?", + "deleteDescription": "{{name}} será eliminado permanentemente para todos e não poderá ser anulado.", + "invitedTo": "Foi convidado para \"{{name}}\"", + "guest": { + "title": "Reunião", + "waiting": "À espera do apresentador...", + "linkInvalid": "Este link de convidado é inválido ou a reunião já terminou." + }, + "reopenFromPanel": "Esta aba da reunião expirou. Volte a abrir a sala no painel Reuniões.", + "requestControl": "Controle de solicitação", + "cancelControlRequest": "Cancelar pedido", + "controlQueue": "Solicitações de controlo", + "noControlRequests": "Não há pedidos pendentes.", + "grantControlTo": "Dê o controlo a {{name}}", + "dismissControlRequest": "Rejeitar pedido de controlo de {{name}}", + "releaseControl": "Libertar controle", + "grantControl": "Dê o controlo", + "revokeControl": "Retome o comando", + "controlRequestedBy": "{{name}} solicitou o controlo", + "grant": "Subvenção", + "controlBadge": "No controlo", + "youArePresenting": "Está a apresentar em outra aba. Pare de apresentar para passar a palavra.", + "created": "Quarto criado", + "invited": "Membros convidados", + "roomTab": "Reunião" + }, + "nav": { + "collab": "Reuniões" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Permitir partilha de sessão", + "description": "Permite que as sessões de terminal ao vivo, RDP, VNC e Telnet sejam partilhadas em toda a instância. Substitui todas as definições de partilha por host quando desativada." + } + }, + "host": { + "allowSessionSharing": { + "label": "Permitir partilha de sessão", + "description": "Permitir que as sessões em direto neste servidor sejam partilhadas através de link ou com outros utilizadores." + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/ro_RO.json b/plugins/session-sharing/locales/translated/ro_RO.json new file mode 100644 index 000000000..9cc89af1e --- /dev/null +++ b/plugins/session-sharing/locales/translated/ro_RO.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Conectare la sesiunea partajată...", + "linkInvalid": "Acest link de partajare este invalid, a expirat sau a fost revocat.", + "rateLimited": "Prea multe încercări, vă rugăm să încercați din nou în scurt timp", + "sessionEnded": "Această sesiune s-a încheiat", + "readOnlyBadge": "Doar vizualizare", + "ownerLabel": "Proprietarul gazdei" + }, + "modalTitle": "Partajați sesiunea", + "shareButton": "Distribuie", + "notReadyToShare": "Sesiunea nu este încă gata de partajare", + "modeTab": { + "link": "Legătură", + "user": "Utilizator" + }, + "linkModeDescription": "Oricine are acest link se poate alătura, nu este necesar un cont.", + "userModeDescription": "Partajați cu un anumit utilizator care are deja acces la această gazdă. Dacă nu are încă acces, partajați mai întâi gazda cu acesta sau utilizați un link în schimb. După partajare, sesiunea apare în fila Conexiuni.", + "permissionLevel": { + "label": "Nivel de permisiune", + "readOnly": "Doar citire", + "readOnlyDescription": "Pot urmări sesiunea în direct, dar nu pot tasta sau interacționa.", + "readWrite": "Citire-scriere", + "readWriteDescription": "Poate tasta și interacționa cu sesiunea la fel ca proprietarul." + }, + "expiryLabel": "Expirarea linkului", + "createLinkButton": "Creați un link", + "createShareButton": "Partajează cu utilizatorul", + "searchUsersPlaceholder": "Căutați utilizatori...", + "noUsersFound": "Nu au fost găsiți utilizatori", + "linkCreated": "Link de partajare creat", + "linkCopied": "Linkul a fost copiat în clipboard", + "copyLink": "Copiază linkul", + "shareCreated": "Sesiune partajată. Va apărea în fila Conexiuni.", + "shareFailed": "Nu s-a putut crea partajarea", + "userLacksHostAccess": "Utilizatorul respectiv nu are încă acces la această gazdă. Partajați mai întâi gazda cu acesta sau folosiți un link în schimb.", + "activeShares": "Acțiuni active", + "noActiveShares": "Nicio partajare activă pentru această sesiune", + "revoke": "Revoca", + "revokeConfirmTitle": "Revocați această partajare?", + "revokeConfirmDescription": "Oricine folosește această partajare va pierde accesul imediat.", + "revoked": "Acțiune revocată", + "revokeFailed": "Nu s-a putut revoca partajarea", + "joinCount": "{{count}} alătură-te", + "joinCount_other": "{{count}} se alătură", + "expiresAt": "Expiră la {{date}}", + "linkShareBadge": "Legătură", + "userShareBadge": "Utilizator: {{username}}", + "loadSharesFailed": "Nu s-au putut încărca partajările active", + "customHoursPlaceholder": "Ore până la expirarea accesului" + }, + "collab": { + "createRoom": "Cameră nouă", + "roomName": "Numele camerei", + "persistentRoom": "Cameră persistentă", + "persistentRoomHint": "Sălile persistente rămân listate după încheierea întâlnirii și pot fi reutilizate.", + "noRooms": "Încă nu există săli. Creează una pentru a începe o întâlnire.", + "openRoom": "Deschide", + "endRoom": "Încheiați întâlnirea", + "leaveRoom": "Pleacă", + "deleteMember": "Elimina", + "removeMember": "Eliminați {{name}}", + "removeMemberTitle": "Eliminați acest membru?", + "removeMemberDescription": "{{name}} va pierde imediat accesul la această cameră. Orice vizualizator SSH activ va fi deconectat.", + "invite": "Invita", + "inviteTitle": "Invită membri", + "members": "Membri", + "membersPanel": "Membrii camerei și solicitările de control", + "membersWithCount": "Membri ({{count}})", + "online": "online", + "offline": "offline", + "you": "(tu)", + "hostBadge": "Gazdă", + "presenterBadge": "Prezentare", + "present": "Prezent", + "presentTitle": "Urcă pe scenă", + "presentHost": "Gazdă", + "searchHosts": "Căutați gazde", + "noHostsFound": "Nu au fost găsite gazde disponibile.", + "presentProtocol": "Protocol", + "stopPresenting": "Opriți prezentarea", + "takeOver": "Prelua", + "takeOverConfirmTitle": "Înlocuiți prezentarea curentă?", + "takeOverDescription": "{{name}} prezintă acum. Preluarea controlului îi va deconecta pe toți de la acea etapă.", + "emptyStage": "Nimeni nu prezintă. Urcă pe scenă pentru a împărtăși o sesiune.", + "stageLoading": "Conectarea la scenă...", + "stageEnded": "Prezentarea s-a încheiat", + "roomEnded": "Această întâlnire s-a încheiat", + "presenterLabel": "{{name}} prezintă", + "guestLink": "Link pentru invitați", + "guestLinkOn": "Linkul pentru invitați este activat. Oricine are linkul poate viziona scena.", + "guestLinkOff": "Linkul pentru invitați este dezactivat.", + "copyLink": "Copiază linkul", + "rotateLink": "Rotiți linkul", + "disableLink": "Dezactivați linkul", + "rotateLinkConfirmTitle": "Rotește linkul pentru oaspeți?", + "rotateLinkDescription": "Legătura curentă va înceta să funcționeze imediat. Oaspeții SSH conectați vor fi deconectați.", + "disableLinkConfirmTitle": "Dezactivați linkul pentru invitați?", + "disableLinkDescription": "Vizualizatorii SSH anonimi vor fi deconectați imediat, iar legătura curentă va înceta să funcționeze.", + "linkCopied": "Linkul a fost copiat", + "roles": "Roluri", + "users": "Utilizatori", + "searchUsers": "Căutați utilizatori", + "endConfirmTitle": "Încheiați această întâlnire?", + "endDescription": "{{name}} se va încheia pentru toată lumea și nu va putea fi redeschis.", + "endPersistentDescription": "Prezentarea activă din {{name}} se va opri. Sala rămâne disponibilă pentru reutilizare.", + "deleteRoom": "Ștergeți camera", + "deleteConfirmTitle": "Ștergeți această cameră?", + "deleteDescription": "{{name}} va fi șters definitiv pentru toată lumea și nu poate fi anulat.", + "invitedTo": "Ai fost invitat la „{{name}}”", + "guest": { + "title": "Întâlnire", + "waiting": "În așteptarea prezentatorului...", + "linkInvalid": "Acest link de invitat este invalid sau întâlnirea s-a încheiat." + }, + "reopenFromPanel": "Această filă de întâlnire a expirat. Redeschideți sala din panoul Întâlniri.", + "requestControl": "Cerere de control", + "cancelControlRequest": "Anulează cererea", + "controlQueue": "Cereri de control", + "noControlRequests": "Nicio solicitare în așteptare.", + "grantControlTo": "Acordă controlul către {{name}}", + "dismissControlRequest": "Închideți solicitarea de control a utilizatorului {{name}}", + "releaseControl": "Controlul eliberării", + "grantControl": "Oferă controlul", + "revokeControl": "Preia controlul", + "controlRequestedBy": "{{name}} a cerut controlul", + "grant": "Acorda", + "controlBadge": "În control", + "youArePresenting": "Prezentați într-o altă filă. Oprește prezentarea pentru a preda scena.", + "created": "Cameră creată", + "invited": "Membrii invitați", + "roomTab": "Întâlnire" + }, + "nav": { + "collab": "Întâlniri" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Permiteți partajarea sesiunii", + "description": "Permite partajarea sesiunilor live de terminal, RDP, VNC și Telnet la nivel de instanță. Suprascrie fiecare comutare de partajare per gazdă atunci când este dezactivată." + } + }, + "host": { + "allowSessionSharing": { + "label": "Permiteți partajarea sesiunii", + "description": "Permiteți partajarea sesiunilor live de pe această gazdă prin link sau cu alți utilizatori" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/ru_RU.json b/plugins/session-sharing/locales/translated/ru_RU.json new file mode 100644 index 000000000..b42df8796 --- /dev/null +++ b/plugins/session-sharing/locales/translated/ru_RU.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Подключение к общей сессии...", + "linkInvalid": "Данная ссылка для общего доступа недействительна, истек срок ее действия или она была аннулирована.", + "rateLimited": "Слишком много попыток, пожалуйста, попробуйте еще раз чуть позже.", + "sessionEnded": "Данная сессия завершена.", + "readOnlyBadge": "Только для просмотра", + "ownerLabel": "Владелец хостинга" + }, + "modalTitle": "Совместное использование сессии", + "shareButton": "Делиться", + "notReadyToShare": "Сессия пока не готова к публикации.", + "modeTab": { + "link": "Связь", + "user": "Пользователь" + }, + "linkModeDescription": "Присоединиться может любой, у кого есть эта ссылка, регистрация не требуется.", + "userModeDescription": "Предоставьте доступ конкретному пользователю, у которого уже есть доступ к этому хосту. Если у него еще нет доступа, сначала предоставьте ему доступ к хосту или используйте ссылку. После предоставления доступа сессия отобразится на вкладке «Подключения» этого пользователя.", + "permissionLevel": { + "label": "Уровень доступа", + "readOnly": "Только для чтения", + "readOnlyDescription": "Можно наблюдать за сессией в прямом эфире, но нельзя печатать или взаимодействовать с ней.", + "readWrite": "Чтение-запись", + "readWriteDescription": "Может печатать и взаимодействовать с сессией так же, как и её владелец." + }, + "expiryLabel": "Срок действия ссылки истекает", + "createLinkButton": "Создать ссылку", + "createShareButton": "Поделиться с пользователем", + "searchUsersPlaceholder": "Поиск пользователей...", + "noUsersFound": "Пользователи не найдены", + "linkCreated": "Ссылка для обмена создана", + "linkCopied": "Ссылка скопирована в буфер обмена", + "copyLink": "Скопировать ссылку", + "shareCreated": "Сессия была проведена совместно. Она отобразится на вкладке «Подключения».", + "shareFailed": "Не удалось создать общий ресурс.", + "userLacksHostAccess": "У этого пользователя пока нет доступа к данному хосту. Сначала предоставьте ему доступ к хосту или используйте ссылку.", + "activeShares": "Активные акции", + "noActiveShares": "В текущей торговой сессии нет активных акций.", + "revoke": "Отменить", + "revokeConfirmTitle": "Отозвать эту публикацию?", + "revokeConfirmDescription": "Любой, кто воспользуется этой ссылкой, немедленно потеряет к ней доступ.", + "revoked": "Акция аннулирована", + "revokeFailed": "Не удалось отозвать акцию.", + "joinCount": "{{count}} присоединиться", + "joinCount_other": "{{count}} присоединяется", + "expiresAt": "Срок действия истекает {{date}}", + "linkShareBadge": "Связь", + "userShareBadge": "Пользователь: {{username}}", + "loadSharesFailed": "Не удалось загрузить активные акции.", + "customHoursPlaceholder": "Часов до окончания срока доступа" + }, + "collab": { + "createRoom": "Новая комната", + "roomName": "Название комнаты", + "persistentRoom": "Постоянное помещение", + "persistentRoomHint": "Список свободных комнат сохраняется и после окончания встречи, и их можно использовать повторно.", + "noRooms": "Комнат пока нет. Создайте комнату, чтобы начать встречу.", + "openRoom": "Открыть", + "endRoom": "Завершение встречи", + "leaveRoom": "Оставлять", + "deleteMember": "Удалять", + "removeMember": "Удалить {{name}}", + "removeMemberTitle": "Удалить этого участника?", + "removeMemberDescription": "{{name}} немедленно потеряет доступ к этой комнате. Любой активный SSH-просмотрщик будет отключен.", + "invite": "Приглашать", + "inviteTitle": "Пригласить участников", + "members": "Члены", + "membersPanel": "Участники комнаты и запросы на управление", + "membersWithCount": "Участники ({{count}})", + "online": "онлайн", + "offline": "офлайн", + "you": "(ты)", + "hostBadge": "Хозяин", + "presenterBadge": "Презентация", + "present": "Подарок", + "presentTitle": "Выходите на сцену", + "presentHost": "Хозяин", + "searchHosts": "Поиск хостов", + "noHostsFound": "Доступных хостов не найдено.", + "presentProtocol": "Протокол", + "stopPresenting": "Прекратите презентацию", + "takeOver": "Перенимать", + "takeOverConfirmTitle": "Заменить текущую презентацию?", + "takeOverDescription": "{{name}} сейчас выступает. Перехват инициативы отключит всех от этой сцены.", + "emptyStage": "Никто не выступает с презентацией. Выходите на сцену, чтобы поделиться своим опытом.", + "stageLoading": "Подключение к сцене...", + "stageEnded": "Презентация завершилась.", + "roomEnded": "Встреча завершена.", + "presenterLabel": "{{name}} представляет", + "guestLink": "Гостевая ссылка", + "guestLinkOn": "Гостевая ссылка включена. Любой, у кого есть ссылка, может посмотреть выступление.", + "guestLinkOff": "Гостевая ссылка отключена.", + "copyLink": "Скопировать ссылку", + "rotateLink": "Повернуть ссылку", + "disableLink": "Отключить ссылку", + "rotateLinkConfirmTitle": "Повернуть гостевую ссылку?", + "rotateLinkDescription": "Текущее соединение немедленно перестанет работать. Подключенные через SSH гостевые системы будут отключены.", + "disableLinkConfirmTitle": "Отключить гостевую ссылку?", + "disableLinkDescription": "Анонимные пользователи SSH-сервера будут немедленно отключены, и текущая связь перестанет работать.", + "linkCopied": "Ссылка скопирована", + "roles": "Роли", + "users": "Пользователи", + "searchUsers": "Поиск пользователей", + "endConfirmTitle": "Закончить это совещание?", + "endDescription": "{{name}} завершится для всех и не может быть возобновлено.", + "endPersistentDescription": "Активная презентация в {{name}} будет остановлена. Комната остается доступной для повторного использования.", + "deleteRoom": "Удалить комнату", + "deleteConfirmTitle": "Удалить эту комнату?", + "deleteDescription": "{{name}} будет безвозвратно удалено для всех и не может быть отменено.", + "invitedTo": "Вас пригласили на \"{{name}}\"", + "guest": { + "title": "Встреча", + "waiting": "Ожидание выступления докладчика...", + "linkInvalid": "Эта гостевая ссылка недействительна, или встреча завершилась." + }, + "reopenFromPanel": "Срок действия этой вкладки «Встречи» истек. Откройте комнату заново на панели «Встречи».", + "requestControl": "Управление запросами", + "cancelControlRequest": "Отменить запрос", + "controlQueue": "Запросы на управление", + "noControlRequests": "Нет ожидающих обработки запросов.", + "grantControlTo": "Передайте управление {{name}}", + "dismissControlRequest": "Отклонить запрос на управление от {{name}}", + "releaseControl": "Управление освобождением", + "grantControl": "Передайте контроль", + "revokeControl": "Верните себе контроль.", + "controlRequestedBy": "{{name}} запросил управление", + "grant": "Грант", + "controlBadge": "Под контролем", + "youArePresenting": "Вы ведёте презентацию в другой вкладке. Прекратите презентацию, чтобы передать слово другому пользователю.", + "created": "Комната создана", + "invited": "Приглашены члены", + "roomTab": "Встреча" + }, + "nav": { + "collab": "Встречи" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Разрешить совместное использование сеанса", + "description": "Разрешить общий доступ к сеансам Live Terminal, RDP, VNC и Telnet для всего экземпляра. При отключении этой функции отменяет все настройки общего доступа для отдельных хостов." + } + }, + "host": { + "allowSessionSharing": { + "label": "Разрешить совместное использование сеанса", + "description": "Разрешите делиться записями онлайн-сессий на этом хостинге по ссылке или с другими пользователями." + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/sr_SP.json b/plugins/session-sharing/locales/translated/sr_SP.json new file mode 100644 index 000000000..c5cfe9ad8 --- /dev/null +++ b/plugins/session-sharing/locales/translated/sr_SP.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Повезивање са дељеном сесијом...", + "linkInvalid": "Овај линк за дељење је неважећи, истекао је или је опозван", + "rateLimited": "Превише покушаја, покушајте поново ускоро", + "sessionEnded": "Ова сесија је завршена", + "readOnlyBadge": "Само преглед", + "ownerLabel": "Власник домаћина" + }, + "modalTitle": "Дели сесију", + "shareButton": "Дели", + "notReadyToShare": "Сесија још није спремна за дељење", + "modeTab": { + "link": "Линк", + "user": "Корисник" + }, + "linkModeDescription": "Свако ко има овај линк може да се придружи, није потребан налог.", + "userModeDescription": "Делите са одређеним корисником који већ има приступ овом хосту. Ако још немају приступ, прво поделите хост са њима или користите линк. Након дељења, сесија се појављује на њиховој картици „Везе“.", + "permissionLevel": { + "label": "Ниво дозволе", + "readOnly": "Само за читање", + "readOnlyDescription": "Може да гледа сесију уживо, али не може да куца или да комуницира.", + "readWrite": "Читање и писање", + "readWriteDescription": "Може да куца и интерагује са сесијом баш као и власник." + }, + "expiryLabel": "Истек линка", + "createLinkButton": "Направи линк", + "createShareButton": "Подели са корисником", + "searchUsersPlaceholder": "Претражи кориснике...", + "noUsersFound": "Није пронађен ниједан корисник", + "linkCreated": "Линк за дељење је направљен", + "linkCopied": "Линк је копиран у међуспремник", + "copyLink": "Копирај линк", + "shareCreated": "Сесија је дељена. Појавиће се на њиховој картици „Везе“.", + "shareFailed": "Није успело креирање дељења", + "userLacksHostAccess": "Тај корисник још увек нема приступ овом хосту. Прво поделите хост са њим или користите линк.", + "activeShares": "Активне акције", + "noActiveShares": "Нема активних дељења за ову сесију", + "revoke": "Поништи", + "revokeConfirmTitle": "Да ли желите да опозовете ово дељење?", + "revokeConfirmDescription": "Свако ко користи ово дељење ће одмах изгубити приступ.", + "revoked": "Дељење опозвано", + "revokeFailed": "Опозивање дељења није успело", + "joinCount": "{{count}} придружи се", + "joinCount_other": "{{count}} придруживања", + "expiresAt": "Истиче {{date}}", + "linkShareBadge": "Линк", + "userShareBadge": "Корисник: {{username}}", + "loadSharesFailed": "Учитавање активних дељења није успело", + "customHoursPlaceholder": "Сати до истека приступа" + }, + "collab": { + "createRoom": "Нова соба", + "roomName": "Назив собе", + "persistentRoom": "Трајна соба", + "persistentRoomHint": "Трајне собе остају наведене након завршетка састанка и могу се поново користити.", + "noRooms": "Још нема соба. Направите једну да бисте започели састанак.", + "openRoom": "Отворено", + "endRoom": "Заврши састанак", + "leaveRoom": "Одиђи", + "deleteMember": "Уклони", + "removeMember": "Уклони {{name}}", + "removeMemberTitle": "Уклонити овог члана?", + "removeMemberDescription": "{{name}} ће одмах изгубити приступ овој соби. Сваки активни SSH гледалац ће бити искључен.", + "invite": "Позови", + "inviteTitle": "Позовите чланове", + "members": "Чланови", + "membersPanel": "Чланови собе и захтеви за контролу", + "membersWithCount": "Чланови ({{count}})", + "online": "онлајн", + "offline": "офлајн", + "you": "(ти)", + "hostBadge": "Домаћин", + "presenterBadge": "Презентација", + "present": "Присутан", + "presentTitle": "Изађи на сцену", + "presentHost": "Домаћин", + "searchHosts": "Претрага хостова", + "noHostsFound": "Није пронађен ниједан доступан хост.", + "presentProtocol": "Протокол", + "stopPresenting": "Заустави презентацију", + "takeOver": "Преузми", + "takeOverConfirmTitle": "Заменити тренутну презентацију?", + "takeOverDescription": "{{name}} тренутно презентује. Преузимање ће искључити све са те сцене.", + "emptyStage": "Нико не презентује. Изађите на бину да бисте поделили сесију.", + "stageLoading": "Повезивање са сценом...", + "stageEnded": "Презентација је завршена", + "roomEnded": "Овај састанак је завршен", + "presenterLabel": "{{name}} презентује", + "guestLink": "Гостујући линк", + "guestLinkOn": "Линк за госте је укључен. Свако ко има линк може да гледа сцену.", + "guestLinkOff": "Линк за госте је искључен.", + "copyLink": "Копирај линк", + "rotateLink": "Ротирај линк", + "disableLink": "Онемогући везу", + "rotateLinkConfirmTitle": "Ротирати линк за госте?", + "rotateLinkDescription": "Тренутна веза ће одмах престати да ради. Повезани SSH гости ће бити искључени.", + "disableLinkConfirmTitle": "Онемогућити линк за госте?", + "disableLinkDescription": "Анонимни SSH гледаоци ће одмах бити прекинути везу и тренутна веза ће престати да ради.", + "linkCopied": "Линк је копиран", + "roles": "Улоге", + "users": "Корисници", + "searchUsers": "Претрага корисника", + "endConfirmTitle": "Да ли желите да завршите овај састанак?", + "endDescription": "{{name}} ће бити завршен за све и не може се поново отворити.", + "endPersistentDescription": "Активна презентација у {{name}} ће се зауставити. Просторија остаје доступна за поновну употребу.", + "deleteRoom": "Обриши собу", + "deleteConfirmTitle": "Да ли желите да избришете ову собу?", + "deleteDescription": "{{name}} ће бити трајно обрисан за све и не може се поништити.", + "invitedTo": "Позвани сте у „{{name}}“", + "guest": { + "title": "Састанак", + "waiting": "Чекање водитеља...", + "linkInvalid": "Овај линк за госте је неважећи или је састанак завршен" + }, + "reopenFromPanel": "Ова картица састанка је истекла. Поново отворите просторију из панела Састанци.", + "requestControl": "Захтевај контролу", + "cancelControlRequest": "Откажи захтев", + "controlQueue": "Захтеви за контролу", + "noControlRequests": "Нема захтева на чекању.", + "grantControlTo": "Дајте контролу кориснику {{name}}", + "dismissControlRequest": "Одбаци захтев за контролу корисника {{name}}", + "releaseControl": "Контрола ослобађања", + "grantControl": "Дајте контролу", + "revokeControl": "Преузмите контролу назад", + "controlRequestedBy": "{{name}} је затражио контролу", + "grant": "Грант", + "controlBadge": "Под контролом", + "youArePresenting": "Презентујете у другој картици. Прекините презентацију да бисте предали сцену.", + "created": "Соба је направљена", + "invited": "Чланови позвани", + "roomTab": "Састанак" + }, + "nav": { + "collab": "Састанци" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Дозволи дељење сесије", + "description": "Дозвољава дељење сесија уживо терминала, RDP, VNC и Telnet сесија на нивоу целе инстанце. Замењује сваки прекидач за дељење по хосту када је онемогућен." + } + }, + "host": { + "allowSessionSharing": { + "label": "Дозволи дељење сесије", + "description": "Дозволите дељење сесија уживо на овом хосту путем линка или са другим корисницима" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/sv_SE.json b/plugins/session-sharing/locales/translated/sv_SE.json new file mode 100644 index 000000000..9eb3e364d --- /dev/null +++ b/plugins/session-sharing/locales/translated/sv_SE.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Ansluter till delad session...", + "linkInvalid": "Den här delningslänken är ogiltig, har löpt ut eller har återkallats", + "rateLimited": "För många försök, försök igen snart", + "sessionEnded": "Den här sessionen är avslutad", + "readOnlyBadge": "Endast visning", + "ownerLabel": "Värdägare" + }, + "modalTitle": "Dela session", + "shareButton": "Dela", + "notReadyToShare": "Sessionen är inte redo att delas ännu", + "modeTab": { + "link": "Länk", + "user": "Användare" + }, + "linkModeDescription": "Vem som helst med den här länken kan gå med, inget konto krävs.", + "userModeDescription": "Dela med en specifik användare som redan har åtkomst till den här värden. Om de inte har åtkomst ännu, dela värden med dem först eller använd en länk istället. När sessionen har delats visas den på deras flik Anslutningar.", + "permissionLevel": { + "label": "Behörighetsnivå", + "readOnly": "Skrivskyddad", + "readOnlyDescription": "Kan titta på sessionen live men kan inte skriva eller interagera.", + "readWrite": "Läs-skriv", + "readWriteDescription": "Kan skriva och interagera med sessionen precis som ägaren." + }, + "expiryLabel": "Länkens utgångsdatum", + "createLinkButton": "Skapa länk", + "createShareButton": "Dela med användaren", + "searchUsersPlaceholder": "Sök användare...", + "noUsersFound": "Inga användare hittades", + "linkCreated": "Delningslänk skapad", + "linkCopied": "Länk kopierad till urklipp", + "copyLink": "Kopiera länk", + "shareCreated": "Sessionen delade. Den kommer att visas på fliken Anslutningar.", + "shareFailed": "Misslyckades med att skapa delningen", + "userLacksHostAccess": "Den användaren har inte åtkomst till den här värden ännu. Dela värden med dem först, eller använd en länk istället.", + "activeShares": "Aktiva aktier", + "noActiveShares": "Inga aktiva delningar för den här sessionen", + "revoke": "Återkalla", + "revokeConfirmTitle": "Återkalla den här delningen?", + "revokeConfirmDescription": "Alla som använder den här delningen kommer att förlora åtkomsten omedelbart.", + "revoked": "Delning återkallad", + "revokeFailed": "Misslyckades med att återkalla delningen", + "joinCount": "{{count}} gå med", + "joinCount_other": "{{count}} ansluter sig", + "expiresAt": "Utgångsdatum {{date}}", + "linkShareBadge": "Länk", + "userShareBadge": "Användare: {{username}}", + "loadSharesFailed": "Misslyckades med att läsa in aktiva delningar", + "customHoursPlaceholder": "Timmar tills åtkomsten upphör" + }, + "collab": { + "createRoom": "Nytt rum", + "roomName": "Rumsnamn", + "persistentRoom": "Permanent rum", + "persistentRoomHint": "Beständiga rum finns kvar i listan efter att mötet är slut och kan återanvändas.", + "noRooms": "Inga rum ännu. Skapa ett för att starta ett möte.", + "openRoom": "Öppna", + "endRoom": "Avsluta mötet", + "leaveRoom": "Lämna", + "deleteMember": "Ta bort", + "removeMember": "Ta bort {{name}}", + "removeMemberTitle": "Ta bort den här medlemmen?", + "removeMemberDescription": "{{name}} kommer att förlora åtkomst till det här rummet omedelbart. Alla aktiva SSH-läsare kommer att kopplas bort.", + "invite": "Bjuda", + "inviteTitle": "Bjud in medlemmar", + "members": "Medlemmar", + "membersPanel": "Rumsmedlemmar och kontrollförfrågningar", + "membersWithCount": "Medlemmar ({{count}})", + "online": "online", + "offline": "off-line", + "you": "(du)", + "hostBadge": "Värd", + "presenterBadge": "Presenterar", + "present": "Presentera", + "presentTitle": "Inta scenen", + "presentHost": "Värd", + "searchHosts": "Sök värdar", + "noHostsFound": "Inga tillgängliga värdar hittades.", + "presentProtocol": "Protokoll", + "stopPresenting": "Sluta presentera", + "takeOver": "Ta över", + "takeOverConfirmTitle": "Ersätta den aktuella presentationen?", + "takeOverDescription": "{{name}} presenterar nu. Att ta över kommer att koppla bort alla från den scenen.", + "emptyStage": "Ingen presenterar. Gå ut på scenen för att dela en session.", + "stageLoading": "Ansluter till scenen...", + "stageEnded": "Presentationen avslutades", + "roomEnded": "Det här mötet har avslutats", + "presenterLabel": "{{name}} presenterar", + "guestLink": "Gästlänk", + "guestLinkOn": "Gästlänken är aktiverad. Alla med länken kan titta på scenen.", + "guestLinkOff": "Gästlänken är avstängd.", + "copyLink": "Kopiera länk", + "rotateLink": "Rotera länken", + "disableLink": "Inaktivera länk", + "rotateLinkConfirmTitle": "Rotera gästlänken?", + "rotateLinkDescription": "Den aktuella länken kommer att sluta fungera omedelbart. Anslutna SSH-gäster kommer att kopplas bort.", + "disableLinkConfirmTitle": "Inaktivera gästlänken?", + "disableLinkDescription": "Anonyma SSH-användare kommer att kopplas bort omedelbart och den aktuella länken kommer att sluta fungera.", + "linkCopied": "Länk kopierad", + "roles": "Roller", + "users": "Användare", + "searchUsers": "Sök användare", + "endConfirmTitle": "Avsluta det här mötet?", + "endDescription": "{{name}} kommer att upphöra för alla och kan inte öppnas igen.", + "endPersistentDescription": "Den aktiva presentationen i {{name}} kommer att stoppas. Rummet är fortfarande tillgängligt för återanvändning.", + "deleteRoom": "Ta bort rum", + "deleteConfirmTitle": "Radera det här rummet?", + "deleteDescription": "{{name}} kommer att raderas permanent för alla och kan inte ångras.", + "invitedTo": "Du blev inbjuden till \"{{name}}\"", + "guest": { + "title": "Möte", + "waiting": "Väntar på programledaren...", + "linkInvalid": "Denna gästlänk är ogiltig eller så har mötet avslutats" + }, + "reopenFromPanel": "Den här mötesfliken har upphört att gälla. Öppna rummet igen från panelen Möten.", + "requestControl": "Begäran om kontroll", + "cancelControlRequest": "Avbryt begäran", + "controlQueue": "Kontrollförfrågningar", + "noControlRequests": "Inga väntande förfrågningar.", + "grantControlTo": "Ge kontroll till {{name}}", + "dismissControlRequest": "Avvisa kontrollbegäran från {{name}}", + "releaseControl": "Släppkontroll", + "grantControl": "Ge kontroll", + "revokeControl": "Ta tillbaka kontrollen", + "controlRequestedBy": "{{name}} bad om kontroll", + "grant": "Bevilja", + "controlBadge": "I kontroll", + "youArePresenting": "Du presenterar i en annan flik. Avbryt presentationen för att lämna över scenen.", + "created": "Rum skapat", + "invited": "Medlemmar inbjudna", + "roomTab": "Möte" + }, + "nav": { + "collab": "Möten" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Tillåt sessionsdelning", + "description": "Tillåt att liveterminal-, RDP-, VNC- och Telnet-sessioner delas instansövergripande. Åsidosätter alla delningsknappar per värd när de är inaktiverade." + } + }, + "host": { + "allowSessionSharing": { + "label": "Tillåt sessionsdelning", + "description": "Tillåt att livesessioner på den här värden delas via länk eller med andra användare" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/th_TH.json b/plugins/session-sharing/locales/translated/th_TH.json new file mode 100644 index 000000000..776790ff8 --- /dev/null +++ b/plugins/session-sharing/locales/translated/th_TH.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "กำลังเชื่อมต่อกับเซสชันที่ใช้ร่วมกัน...", + "linkInvalid": "ลิงก์แชร์นี้ไม่ถูกต้อง หมดอายุ หรือถูกยกเลิกแล้ว", + "rateLimited": "พยายามหลายครั้งเกินไป โปรดลองใหม่อีกครั้งในภายหลัง", + "sessionEnded": "การประชุมครั้งนี้สิ้นสุดลงแล้ว", + "readOnlyBadge": "ดูได้อย่างเดียว", + "ownerLabel": "เจ้าของที่พัก" + }, + "modalTitle": "เซสชั่นการแบ่งปัน", + "shareButton": "แบ่งปัน", + "notReadyToShare": "เซสชั่นยังไม่พร้อมให้แชร์", + "modeTab": { + "link": "ลิงก์", + "user": "ผู้ใช้" + }, + "linkModeDescription": "ใครก็ตามที่มีลิงก์นี้สามารถเข้าร่วมได้ ไม่จำเป็นต้องมีบัญชีผู้ใช้", + "userModeDescription": "แชร์กับผู้ใช้เฉพาะรายที่มีสิทธิ์เข้าถึงโฮสต์นี้อยู่แล้ว หากพวกเขายังไม่มีสิทธิ์เข้าถึง ให้แชร์โฮสต์กับพวกเขาก่อน หรือใช้ลิงก์แทน เมื่อแชร์แล้ว เซสชันจะปรากฏในแท็บการเชื่อมต่อของพวกเขา", + "permissionLevel": { + "label": "ระดับการอนุญาต", + "readOnly": "อ่านอย่างเดียว", + "readOnlyDescription": "สามารถรับชมการประชุมสดได้ แต่ไม่สามารถพิมพ์หรือโต้ตอบได้", + "readWrite": "อ่าน-เขียน", + "readWriteDescription": "สามารถพิมพ์และโต้ตอบกับเซสชันได้เหมือนกับเจ้าของเซสชัน" + }, + "expiryLabel": "ลิงก์หมดอายุ", + "createLinkButton": "สร้างลิงก์", + "createShareButton": "แชร์กับผู้ใช้", + "searchUsersPlaceholder": "ค้นหาผู้ใช้...", + "noUsersFound": "ไม่พบผู้ใช้งาน", + "linkCreated": "สร้างลิงก์สำหรับแชร์แล้ว", + "linkCopied": "คัดลอกลิงก์ไปยังคลิปบอร์ดแล้ว", + "copyLink": "คัดลิงก์", + "shareCreated": "แชร์เซสชันแล้ว เซสชันนั้นจะปรากฏในแท็บการเชื่อมต่อของพวกเขา", + "shareFailed": "ไม่สามารถสร้างการแชร์ได้", + "userLacksHostAccess": "ผู้ใช้รายนั้นยังไม่มีสิทธิ์เข้าถึงโฮสต์นี้ โปรดแชร์โฮสต์ให้พวกเขาก่อน หรือใช้ลิงก์แทน", + "activeShares": "หุ้นที่ใช้งานอยู่", + "noActiveShares": "ไม่มีหุ้นที่เปิดซื้อขายในรอบนี้", + "revoke": "ถอน", + "revokeConfirmTitle": "ต้องการยกเลิกการแชร์นี้หรือไม่?", + "revokeConfirmDescription": "ใครก็ตามที่ใช้ลิงก์นี้จะเสียสิทธิ์การเข้าถึงทันที", + "revoked": "หุ้นถูกเพิกถอน", + "revokeFailed": "ไม่สามารถเพิกถอนหุ้นได้", + "joinCount": "{{count}} เข้าร่วม", + "joinCount_other": "{{count}} เข้าร่วม", + "expiresAt": "หมดอายุ {{date}}", + "linkShareBadge": "ลิงก์", + "userShareBadge": "ผู้ใช้: {{username}}", + "loadSharesFailed": "ไม่สามารถโหลดหุ้นที่ใช้งานอยู่ได้", + "customHoursPlaceholder": "เหลือเวลาอีกกี่ชั่วโมงก่อนที่สิทธิ์การเข้าถึงจะหมดอายุ" + }, + "collab": { + "createRoom": "ห้องใหม่", + "roomName": "ชื่อห้อง", + "persistentRoom": "ห้องที่คงอยู่", + "persistentRoomHint": "ห้องประชุมที่ตั้งค่าไว้ถาวรจะยังคงอยู่ในรายการหลังจากสิ้นสุดการประชุมและสามารถนำกลับมาใช้ใหม่ได้", + "noRooms": "ยังไม่มีห้องประชุม สร้างห้องประชุมเพื่อเริ่มการประชุม", + "openRoom": "เปิด", + "endRoom": "สิ้นสุดการประชุม", + "leaveRoom": "ออกจาก", + "deleteMember": "ลบ", + "removeMember": "ลบ {{name}}", + "removeMemberTitle": "ลบสมาชิกนี้ออก?", + "removeMemberDescription": "{{name}} จะเสียสิทธิ์ในการเข้าถึงห้องนี้ทันที โปรแกรมดู SSH ที่ใช้งานอยู่จะถูกตัดการเชื่อมต่อ", + "invite": "เชิญ", + "inviteTitle": "เชิญสมาชิก", + "members": "สมาชิก", + "membersPanel": "สมาชิกห้องและคำขอควบคุม", + "membersWithCount": "สมาชิก ({{count}})", + "online": "ออนไลน์", + "offline": "ออฟไลน์", + "you": "(คุณ)", + "hostBadge": "เจ้าภาพ", + "presenterBadge": "นำเสนอ", + "present": "ปัจจุบัน", + "presentTitle": "ขึ้นเวที", + "presentHost": "เจ้าภาพ", + "searchHosts": "ค้นหาโฮสต์", + "noHostsFound": "ไม่พบโฮสต์ที่ใช้งานได้", + "presentProtocol": "โปรโตคอล", + "stopPresenting": "หยุดการนำเสนอ", + "takeOver": "เข้ายึดครอง", + "takeOverConfirmTitle": "ต้องการเปลี่ยนรูปแบบการนำเสนอปัจจุบันหรือไม่?", + "takeOverDescription": "{{name}} กำลังนำเสนออยู่ การเข้าควบคุมจะตัดการเชื่อมต่อทุกคนจากเวทีนั้น", + "emptyStage": "ไม่มีใครกำลังนำเสนออยู่ เชิญขึ้นเวทีเพื่อร่วมแบ่งปันช่วงการนำเสนอได้เลย", + "stageLoading": "เชื่อมต่อกับเวที...", + "stageEnded": "การนำเสนอสิ้นสุดลงแล้ว", + "roomEnded": "การประชุมนี้ได้สิ้นสุดลงแล้ว", + "presenterLabel": "{{name}} กำลังนำเสนอ", + "guestLink": "ลิงก์สำหรับแขก", + "guestLinkOn": "ลิงก์สำหรับแขกรับเชิญเปิดใช้งานแล้ว ใครก็ตามที่มีลิงก์สามารถรับชมการแสดงสดได้", + "guestLinkOff": "ลิงก์สำหรับผู้เยี่ยมชมถูกปิดใช้งาน", + "copyLink": "คัดลิงก์", + "rotateLink": "หมุนลิงก์", + "disableLink": "ปิดใช้งานลิงก์", + "rotateLinkConfirmTitle": "หมุนลิงก์สำหรับแขกรับเชิญหรือไม่?", + "rotateLinkDescription": "ลิงก์ปัจจุบันจะหยุดทำงานทันที ผู้ใช้งาน SSH ที่เชื่อมต่ออยู่จะถูกตัดการเชื่อมต่อ", + "disableLinkConfirmTitle": "ปิดใช้งานลิงก์สำหรับแขกใช่ไหม?", + "disableLinkDescription": "ผู้ใช้งาน SSH ที่ไม่ระบุตัวตนจะถูกตัดการเชื่อมต่อทันที และลิงก์ปัจจุบันจะหยุดทำงาน", + "linkCopied": "คัดลอกลิงก์แล้ว", + "roles": "บทบาท", + "users": "ผู้ใช้", + "searchUsers": "ค้นหาผู้ใช้", + "endConfirmTitle": "จบการประชุมนี้?", + "endDescription": "{{name}} จะสิ้นสุดลงสำหรับทุกคนและไม่สามารถเปิดใช้งานใหม่ได้อีก", + "endPersistentDescription": "การนำเสนอใน {{name}} จะหยุดลง ห้องยังคงพร้อมใช้งานได้ตามปกติ", + "deleteRoom": "ลบห้อง", + "deleteConfirmTitle": "ลบห้องนี้?", + "deleteDescription": "{{name}} จะถูกลบอย่างถาวรสำหรับทุกคนและไม่สามารถกู้คืนได้", + "invitedTo": "คุณได้รับเชิญไปที่ \"{{name}}\"", + "guest": { + "title": "การประชุม", + "waiting": "กำลังรอผู้ประกาศ...", + "linkInvalid": "ลิงก์สำหรับผู้เข้าร่วมนี้ไม่ถูกต้อง หรือการประชุมได้สิ้นสุดลงแล้ว" + }, + "reopenFromPanel": "แท็บการประชุมนี้หมดอายุแล้ว โปรดเปิดห้องประชุมอีกครั้งจากแผงการประชุม", + "requestControl": "การควบคุมคำขอ", + "cancelControlRequest": "คำขอการยกเลิก", + "controlQueue": "คำขอควบคุม", + "noControlRequests": "ไม่มีคำขอที่รอการดำเนินการ", + "grantControlTo": "มอบการควบคุมให้กับ {{name}}", + "dismissControlRequest": "ยกเลิกคำขอควบคุมของ {{name}}", + "releaseControl": "ปล่อยการควบคุม", + "grantControl": "มอบการควบคุม", + "revokeControl": "ทวงคืนการควบคุม", + "controlRequestedBy": "{{name}} ขอควบคุม", + "grant": "ยินยอม", + "controlBadge": "ควบคุมได้", + "youArePresenting": "คุณกำลังนำเสนออยู่ในแท็บอื่น โปรดหยุดการนำเสนอเพื่อส่งต่อให้ผู้อื่น", + "created": "สร้างห้อง", + "invited": "สมาชิกที่ได้รับเชิญ", + "roomTab": "การประชุม" + }, + "nav": { + "collab": "การประชุม" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "อนุญาตการแชร์เซสชัน", + "description": "อนุญาตให้แชร์เซสชันเทอร์มินัลสด, RDP, VNC และ Telnet ทั่วทั้งอินสแตนซ์ การตั้งค่านี้จะแทนที่การตั้งค่าการแชร์ต่อโฮสต์ทั้งหมดเมื่อปิดใช้งานอยู่" + } + }, + "host": { + "allowSessionSharing": { + "label": "อนุญาตการแชร์เซสชัน", + "description": "อนุญาตให้แชร์เซสชันสดบนโฮสต์นี้ผ่านลิงก์หรือกับผู้ใช้รายอื่น" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/tr_TR.json b/plugins/session-sharing/locales/translated/tr_TR.json new file mode 100644 index 000000000..f77953bbd --- /dev/null +++ b/plugins/session-sharing/locales/translated/tr_TR.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Paylaşılan oturuma bağlanılıyor...", + "linkInvalid": "Bu paylaşım bağlantısı geçersiz, süresi dolmuş veya iptal edilmiş.", + "rateLimited": "Çok fazla deneme yapıldı, lütfen kısa süre sonra tekrar deneyin.", + "sessionEnded": "Bu oturum sona erdi.", + "readOnlyBadge": "Yalnızca görüntüle", + "ownerLabel": "Ev sahibi" + }, + "modalTitle": "Oturumu paylaş", + "shareButton": "Paylaşmak", + "notReadyToShare": "Oturum henüz paylaşılmaya hazır değil.", + "modeTab": { + "link": "Bağlantı", + "user": "Kullanıcı" + }, + "linkModeDescription": "Bu bağlantıya sahip herkes katılabilir, hesap gerekmez.", + "userModeDescription": "Bu sunucuya zaten erişimi olan belirli bir kullanıcıyla paylaşın. Henüz erişimleri yoksa, önce sunucuyu onlarla paylaşın veya bunun yerine bir bağlantı kullanın. Paylaşıldıktan sonra, oturum Bağlantılar sekmesinde görünür.", + "permissionLevel": { + "label": "İzin seviyesi", + "readOnly": "salt okunur", + "readOnlyDescription": "Oturumu canlı olarak izleyebilirsiniz ancak yazı yazamaz veya etkileşimde bulunamazsınız.", + "readWrite": "Okuma-yazma", + "readWriteDescription": "Oturum sahibi gibi yazabilir ve oturumla etkileşim kurabilirsiniz." + }, + "expiryLabel": "Bağlantı süresinin dolması", + "createLinkButton": "Bağlantı oluştur", + "createShareButton": "Kullanıcıyla paylaş", + "searchUsersPlaceholder": "Kullanıcıları ara...", + "noUsersFound": "Hiç kullanıcı bulunamadı.", + "linkCreated": "Paylaşım bağlantısı oluşturuldu.", + "linkCopied": "Bağlantı panoya kopyalandı", + "copyLink": "Bağlantıyı kopyala", + "shareCreated": "Oturum paylaşıldı. Bu, onların Bağlantılar sekmesinde görünecektir.", + "shareFailed": "Paylaşım oluşturulamadı.", + "userLacksHostAccess": "Bu kullanıcının henüz bu sunucuya erişimi yok. Öncelikle sunucuyu onlarla paylaşın veya bunun yerine bir bağlantı kullanın.", + "activeShares": "Aktif hisseler", + "noActiveShares": "Bu oturum için aktif hisse senedi bulunmamaktadır.", + "revoke": "Geri çekmek", + "revokeConfirmTitle": "Bu hisseyi iptal etmek ister misiniz?", + "revokeConfirmDescription": "Bu paylaşımı kullanan herkes erişimini anında kaybedecektir.", + "revoked": "Hisseler geri alındı", + "revokeFailed": "Hisseleri geri alma işlemi başarısız oldu.", + "joinCount": "{{count}} katıl", + "joinCount_other": "{{count}} birleşiyor", + "expiresAt": "Süresi doluyor {{date}}", + "linkShareBadge": "Bağlantı", + "userShareBadge": "Kullanıcı: {{username}}", + "loadSharesFailed": "Aktif paylaşımlar yüklenemedi.", + "customHoursPlaceholder": "Erişim süresi (saat)" + }, + "collab": { + "createRoom": "Yeni oda", + "roomName": "Oda adı", + "persistentRoom": "Kalıcı oda", + "persistentRoomHint": "Kalıcı olarak tahsis edilen odalar, toplantı bittikten sonra da listede kalır ve tekrar kullanılabilir.", + "noRooms": "Henüz oda yok. Toplantı başlatmak için bir oda oluşturun.", + "openRoom": "Açık", + "endRoom": "Toplantıyı sonlandırın", + "leaveRoom": "Ayrılmak", + "deleteMember": "Kaldırmak", + "removeMember": "{{name}}'ı kaldır", + "removeMemberTitle": "Bu üyeyi kaldırmak mı istiyorsunuz?", + "removeMemberDescription": "{{name}} bu odaya erişimini derhal kaybedecektir. Aktif olan tüm SSH izleyicilerinin bağlantısı kesilecektir.", + "invite": "Davet etmek", + "inviteTitle": "Üyeleri davet edin", + "members": "Üyeler", + "membersPanel": "Oda üyeleri ve kontrol talepleri", + "membersWithCount": "Üyeler ({{count}})", + "online": "çevrimiçi", + "offline": "çevrimdışı", + "you": "(Sen)", + "hostBadge": "Ev sahibi", + "presenterBadge": "Sunar", + "present": "Sunmak", + "presentTitle": "Sahneye çıkın", + "presentHost": "Ev sahibi", + "searchHosts": "Ev sahiplerini ara", + "noHostsFound": "Kullanılabilir sunucu bulunamadı.", + "presentProtocol": "Protokol", + "stopPresenting": "Sunum yapmayı bırakın", + "takeOver": "Devralmak", + "takeOverConfirmTitle": "Mevcut sunumu değiştirmek mi istiyorsunuz?", + "takeOverDescription": "{{name}} şu anda sunum yapıyor. Devralmak, herkesi o sahneden ayıracaktır.", + "emptyStage": "Kimse sunum yapmıyor. Sahneye çıkın ve bir sunum paylaşın.", + "stageLoading": "Sahneye bağlanmak...", + "stageEnded": "Sunum sona erdi.", + "roomEnded": "Bu toplantı sona erdi.", + "presenterLabel": "{{name}} sunuyor", + "guestLink": "Konuk bağlantısı", + "guestLinkOn": "Misafir bağlantısı aktif. Bağlantıya sahip herkes sahneyi izleyebilir.", + "guestLinkOff": "Misafir bağlantısı kapalı.", + "copyLink": "Bağlantıyı kopyala", + "rotateLink": "Bağlantıyı döndür", + "disableLink": "Bağlantıyı devre dışı bırak", + "rotateLinkConfirmTitle": "Konuk bağlantısını döndürelim mi?", + "rotateLinkDescription": "Mevcut bağlantı derhal çalışmayı durduracaktır. Bağlı SSH misafirlerinin bağlantısı kesilecektir.", + "disableLinkConfirmTitle": "Misafir bağlantısını devre dışı bırak?", + "disableLinkDescription": "Anonim SSH izleyicilerinin bağlantısı derhal kesilecek ve mevcut bağlantı çalışmayı durduracaktır.", + "linkCopied": "Bağlantı kopyalandı", + "roles": "Roller", + "users": "Kullanıcılar", + "searchUsers": "Kullanıcıları ara", + "endConfirmTitle": "Bu toplantıyı sonlandıralım mı?", + "endDescription": "{{name}} herkes için sona erecek ve yeniden açılamayacak.", + "endPersistentDescription": "{{name}} 'daki aktif sunum durdurulacaktır. Oda yeniden kullanıma açıktır.", + "deleteRoom": "Odayı sil", + "deleteConfirmTitle": "Bu odayı silmek mi?", + "deleteDescription": "{{name}} herkes için kalıcı olarak silinecek ve geri alınamaz.", + "invitedTo": "\"{{name}} \" etkinliğine davet edildiniz.", + "guest": { + "title": "Toplantı", + "waiting": "Sunucuyu bekliyoruz...", + "linkInvalid": "Bu konuk bağlantısı geçersiz veya toplantı sona erdi." + }, + "reopenFromPanel": "Bu toplantı sekmesinin süresi doldu. Toplantılar panelinden odayı yeniden açın.", + "requestControl": "Talep kontrolü", + "cancelControlRequest": "İptal talebi", + "controlQueue": "Kontrol talepleri", + "noControlRequests": "Bekleyen talep yok.", + "grantControlTo": "Kontrolü {{name}}'a verin.", + "dismissControlRequest": "{{name}}'ın kontrol isteğini reddet", + "releaseControl": "Kontrolü serbest bırak", + "grantControl": "Kontrolü ona verin.", + "revokeControl": "Kontrolü geri alın", + "controlRequestedBy": "{{name}} kontrolü istedi", + "grant": "Hibe etmek", + "controlBadge": "Kontrol altında", + "youArePresenting": "Başka bir sekmede sunum yapıyorsunuz. Sunumu durdurun ve sahneyi karşı tarafa devredin.", + "created": "Oda oluşturuldu", + "invited": "Üyeler davet edildi", + "roomTab": "Toplantı" + }, + "nav": { + "collab": "Toplantılar" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Oturum Paylaşımına İzin Ver", + "description": "Canlı terminal, RDP, VNC ve Telnet oturumlarının tüm örnek genelinde paylaşılmasına izin verin. Devre dışı bırakıldığında, sunucu başına paylaşım ayarlarının tümünü geçersiz kılar." + } + }, + "host": { + "allowSessionSharing": { + "label": "Oturum Paylaşımına İzin Ver", + "description": "Bu sunucudaki canlı oturumların bağlantı yoluyla veya diğer kullanıcılarla paylaşılmasına izin verin." + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/uk_UA.json b/plugins/session-sharing/locales/translated/uk_UA.json new file mode 100644 index 000000000..0a3d4358d --- /dev/null +++ b/plugins/session-sharing/locales/translated/uk_UA.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Підключення до спільного сеансу...", + "linkInvalid": "Це посилання для спільного доступу недійсне, термін його дії минув або його було відкликано", + "rateLimited": "Забагато спроб, спробуйте ще раз пізніше", + "sessionEnded": "Цей сеанс завершився", + "readOnlyBadge": "Тільки перегляд", + "ownerLabel": "Власник хоста" + }, + "modalTitle": "Поділитися сеансом", + "shareButton": "Поділитися", + "notReadyToShare": "Сеанс ще не готовий до поширення", + "modeTab": { + "link": "Посилання", + "user": "Користувач" + }, + "linkModeDescription": "Приєднатися може кожен, хто має це посилання, обліковий запис не потрібен.", + "userModeDescription": "Надайте доступ певному користувачеві, який уже має доступ до цього хоста. Якщо у нього ще немає доступу, спочатку надайте йому доступ до хоста або скористайтеся посиланням. Після надання доступу сеанс відображається на його вкладці «Підключення».", + "permissionLevel": { + "label": "Рівень дозволів", + "readOnly": "Тільки для читання", + "readOnlyDescription": "Можна дивитися сеанс у прямому ефірі, але не можна друкувати чи взаємодіяти.", + "readWrite": "Читання-запис", + "readWriteDescription": "Можна друкувати та взаємодіяти з сеансом так само, як і його власник." + }, + "expiryLabel": "Термін дії посилання", + "createLinkButton": "Створити посилання", + "createShareButton": "Поділитися з користувачем", + "searchUsersPlaceholder": "Пошук користувачів...", + "noUsersFound": "Користувачів не знайдено", + "linkCreated": "Посилання для спільного доступу створено", + "linkCopied": "Посилання скопійовано в буфер обміну", + "copyLink": "Копіювати посилання", + "shareCreated": "Сеанс спільний. Він відображатиметься на вкладці «Зв’язки».", + "shareFailed": "Не вдалося створити спільний доступ", + "userLacksHostAccess": "Цей користувач ще не має доступу до цього хоста. Спочатку надайте йому доступ до хоста або скористайтеся посиланням.", + "activeShares": "Активні акції", + "noActiveShares": "Немає активних поширень для цього сеансу", + "revoke": "Скасувати", + "revokeConfirmTitle": "Скасувати цей спільний доступ?", + "revokeConfirmDescription": "Будь-хто, хто користується цим ресурсом спільного доступу, негайно втратить доступ.", + "revoked": "Акції анульовано", + "revokeFailed": "Не вдалося скасувати спільний доступ", + "joinCount": "{{count}} приєднатися", + "joinCount_other": "{{count}} приєднується", + "expiresAt": "Термін дії закінчується {{date}}", + "linkShareBadge": "Посилання", + "userShareBadge": "Користувач: {{username}}", + "loadSharesFailed": "Не вдалося завантажити активні спільні ресурси", + "customHoursPlaceholder": "Години до закінчення терміну дії доступу" + }, + "collab": { + "createRoom": "Нова кімната", + "roomName": "Назва кімнати", + "persistentRoom": "Постійна кімната", + "persistentRoomHint": "Постійні кімнати залишаються у списку після завершення зустрічі та можуть бути використані повторно.", + "noRooms": "Поки що немає кімнат. Створіть одну, щоб розпочати зустріч.", + "openRoom": "ВІДЧИНЕНО", + "endRoom": "Закінчити зустріч", + "leaveRoom": "Залишити", + "deleteMember": "Видалити", + "removeMember": "Видалити {{name}}", + "removeMemberTitle": "Видалити цього учасника?", + "removeMemberDescription": "{{name}} негайно втратить доступ до цієї кімнати. Будь-який активний переглядач SSH буде відключено.", + "invite": "Запросити", + "inviteTitle": "Запросити учасників", + "members": "Члени", + "membersPanel": "Учасники кімнати та запити на керування", + "membersWithCount": "Учасники ({{count}})", + "online": "онлайн", + "offline": "офлайн", + "you": "(ти)", + "hostBadge": "Хост", + "presenterBadge": "Презентація", + "present": "Подарунок", + "presentTitle": "Вийди на сцену", + "presentHost": "Хост", + "searchHosts": "Пошук хостів", + "noHostsFound": "Не знайдено доступних хостів.", + "presentProtocol": "Протокол", + "stopPresenting": "Зупинити презентацію", + "takeOver": "Захопити", + "takeOverConfirmTitle": "Замінити поточну презентацію?", + "takeOverDescription": "{{name}} зараз презентує. Перехоплення сцени відключить усіх учасників.", + "emptyStage": "Ніхто не виступає. Вийдіть на сцену, щоб поділитися сесією.", + "stageLoading": "Підключення до сцени...", + "stageEnded": "Презентація закінчилася", + "roomEnded": "Ця зустріч завершилася", + "presenterLabel": "{{name}} презентує", + "guestLink": "Гостьове посилання", + "guestLinkOn": "Гостьове посилання ввімкнено. Будь-хто, хто має посилання, може дивитися сцену.", + "guestLinkOff": "Гостьове посилання вимкнено.", + "copyLink": "Копіювати посилання", + "rotateLink": "Повернути посилання", + "disableLink": "Вимкнути посилання", + "rotateLinkConfirmTitle": "Повернути гостьове посилання?", + "rotateLinkDescription": "Поточне посилання негайно перестане працювати. Підключені гості SSH будуть відключені.", + "disableLinkConfirmTitle": "Вимкнути гостьове посилання?", + "disableLinkDescription": "Анонімні користувачі SSH будуть негайно відключені, а поточне посилання перестане працювати.", + "linkCopied": "Посилання скопійовано", + "roles": "Ролі", + "users": "Користувачі", + "searchUsers": "Пошук користувачів", + "endConfirmTitle": "Завершити цю зустріч?", + "endDescription": "{{name}} завершиться для всіх і не може бути відкрито повторно.", + "endPersistentDescription": "Активна презентація в {{name}} буде зупинена. Кімната залишається доступною для повторного використання.", + "deleteRoom": "Видалити кімнату", + "deleteConfirmTitle": "Видалити цю кімнату?", + "deleteDescription": "{{name}} буде видалено назавжди для всіх і цю дію не можна буде скасувати.", + "invitedTo": "Вас запросили до «{{name}}»", + "guest": { + "title": "Зустріч", + "waiting": "Чекаємо на ведучого...", + "linkInvalid": "Це гостьове посилання недійсне або зустріч завершилася" + }, + "reopenFromPanel": "Термін дії цієї вкладки зустрічі закінчився. Відкрийте кімнату знову на панелі Зустрічі.", + "requestControl": "Запит на керування", + "cancelControlRequest": "Скасувати запит", + "controlQueue": "Запити на керування", + "noControlRequests": "Немає запитів, що очікують на розгляд.", + "grantControlTo": "Надайте керування {{name}}", + "dismissControlRequest": "Відхилити запит керування від {{name}}", + "releaseControl": "Контроль випуску", + "grantControl": "Надайте контроль", + "revokeControl": "Поверніть собі контроль", + "controlRequestedBy": "{{name}} запросив керування", + "grant": "Грант", + "controlBadge": "Контроль", + "youArePresenting": "Ви презентуєте в іншій вкладці. Зупиніть презентацію, щоб передати сцену.", + "created": "Кімнату створено", + "invited": "Запрошені учасники", + "roomTab": "Зустріч" + }, + "nav": { + "collab": "Зустрічі" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Дозволити спільний доступ до сеансу", + "description": "Дозволити спільний доступ до сеансів терміналу, RDP, VNC та Telnet для всього екземпляра. Перевизначає кожен перемикач спільного доступу для кожного хоста, якщо його вимкнено." + } + }, + "host": { + "allowSessionSharing": { + "label": "Дозволити спільний доступ до сеансу", + "description": "Дозволити ділитися прямими трансляціями на цьому хості через посилання або з іншими користувачами" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/vi_VN.json b/plugins/session-sharing/locales/translated/vi_VN.json new file mode 100644 index 000000000..024b2f571 --- /dev/null +++ b/plugins/session-sharing/locales/translated/vi_VN.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "Đang kết nối đến phiên chia sẻ...", + "linkInvalid": "Liên kết chia sẻ này không hợp lệ, đã hết hạn hoặc đã bị thu hồi.", + "rateLimited": "Đã thử quá nhiều lần, vui lòng thử lại sau.", + "sessionEnded": "Phiên này đã kết thúc", + "readOnlyBadge": "Chỉ xem", + "ownerLabel": "Chủ nhà" + }, + "modalTitle": "Chia sẻ phiên họp", + "shareButton": "Chia sẻ", + "notReadyToShare": "Buổi học chưa sẵn sàng để chia sẻ.", + "modeTab": { + "link": "Liên kết", + "user": "Người dùng" + }, + "linkModeDescription": "Bất kỳ ai có liên kết này đều có thể tham gia, không cần tài khoản.", + "userModeDescription": "Chia sẻ với một người dùng cụ thể đã có quyền truy cập vào máy chủ này. Nếu họ chưa có quyền truy cập, hãy chia sẻ máy chủ với họ trước hoặc sử dụng liên kết thay thế. Sau khi chia sẻ, phiên làm việc sẽ xuất hiện trong tab Kết nối của họ.", + "permissionLevel": { + "label": "Cấp độ quyền hạn", + "readOnly": "Chỉ đọc", + "readOnlyDescription": "Có thể xem phiên trực tiếp nhưng không thể gõ chữ hoặc tương tác.", + "readWrite": "Đọc-viết", + "readWriteDescription": "Có thể nhập liệu và tương tác với phiên làm việc giống như chủ sở hữu." + }, + "expiryLabel": "Liên kết hết hạn", + "createLinkButton": "Tạo liên kết", + "createShareButton": "Chia sẻ với người dùng", + "searchUsersPlaceholder": "Tìm kiếm người dùng...", + "noUsersFound": "Không tìm thấy người dùng nào", + "linkCreated": "Đã tạo liên kết chia sẻ", + "linkCopied": "Liên kết đã được sao chép vào clipboard", + "copyLink": "Sao chép liên kết", + "shareCreated": "Phiên họp đã được chia sẻ. Nó sẽ xuất hiện trong tab Kết nối của họ.", + "shareFailed": "Không thể tạo chia sẻ", + "userLacksHostAccess": "Người dùng đó chưa có quyền truy cập vào máy chủ này. Hãy chia sẻ thông tin máy chủ với họ trước, hoặc sử dụng liên kết thay thế.", + "activeShares": "Cổ phiếu tích cực", + "noActiveShares": "Hiện không có cổ phiếu nào đang được giao dịch trong phiên này.", + "revoke": "Thu hồi", + "revokeConfirmTitle": "Thu hồi cổ phần này?", + "revokeConfirmDescription": "Bất kỳ ai sử dụng thư mục chia sẻ này sẽ mất quyền truy cập ngay lập tức.", + "revoked": "Cổ phần đã bị thu hồi", + "revokeFailed": "Không thể thu hồi cổ phần", + "joinCount": "{{count}} tham gia", + "joinCount_other": "{{count}} tham gia", + "expiresAt": "Hết hạn {{date}}", + "linkShareBadge": "Liên kết", + "userShareBadge": "Người dùng: {{username}}", + "loadSharesFailed": "Không thể tải các chia sẻ đang hoạt động", + "customHoursPlaceholder": "Số giờ cho đến khi hết hạn truy cập" + }, + "collab": { + "createRoom": "Phòng mới", + "roomName": "Tên phòng", + "persistentRoom": "Phòng bền vững", + "persistentRoomHint": "Các phòng họp được lưu lại sẽ vẫn hiển thị sau khi cuộc họp kết thúc và có thể được sử dụng lại.", + "noRooms": "Chưa có phòng nào. Hãy tạo một phòng để bắt đầu cuộc họp.", + "openRoom": "Mở", + "endRoom": "Kết thúc cuộc họp", + "leaveRoom": "Rời khỏi", + "deleteMember": "Di dời", + "removeMember": "Xóa {{name}}", + "removeMemberTitle": "Xóa thành viên này?", + "removeMemberDescription": "{{name}} sẽ mất quyền truy cập vào phòng này ngay lập tức. Bất kỳ trình xem SSH nào đang hoạt động sẽ bị ngắt kết nối.", + "invite": "Mời", + "inviteTitle": "Mời thành viên", + "members": "Thành viên", + "membersPanel": "Thành viên phòng và yêu cầu kiểm soát", + "membersWithCount": "Thành viên ({{count}})", + "online": "trực tuyến", + "offline": "ngoại tuyến", + "you": "(Bạn)", + "hostBadge": "Chủ nhà", + "presenterBadge": "Giới thiệu", + "present": "Hiện tại", + "presentTitle": "Hãy lên sân khấu", + "presentHost": "Chủ nhà", + "searchHosts": "Tìm kiếm máy chủ", + "noHostsFound": "Không tìm thấy máy chủ nào khả dụng.", + "presentProtocol": "Giao thức", + "stopPresenting": "Dừng thuyết trình", + "takeOver": "Tiếp quản", + "takeOverConfirmTitle": "Bạn muốn thay thế bài thuyết trình hiện tại?", + "takeOverDescription": "{{name}} đang trình bày. Việc tiếp quản sẽ ngắt kết nối mọi người khỏi sân khấu đó.", + "emptyStage": "Hiện chưa có ai thuyết trình. Hãy lên sân khấu để chia sẻ một phiên thảo luận.", + "stageLoading": "Kết nối với sân khấu...", + "stageEnded": "Buổi thuyết trình kết thúc.", + "roomEnded": "Cuộc họp này đã kết thúc.", + "presenterLabel": "{{name}} đang trình bày", + "guestLink": "Liên kết khách", + "guestLinkOn": "Link dành cho khách đã được bật. Bất cứ ai có link đều có thể xem chương trình.", + "guestLinkOff": "Liên kết khách đã bị tắt.", + "copyLink": "Sao chép liên kết", + "rotateLink": "Xoay liên kết", + "disableLink": "Vô hiệu hóa liên kết", + "rotateLinkConfirmTitle": "Xoay liên kết khách?", + "rotateLinkDescription": "Kết nối hiện tại sẽ ngừng hoạt động ngay lập tức. Các máy khách SSH đang kết nối sẽ bị ngắt kết nối.", + "disableLinkConfirmTitle": "Vô hiệu hóa liên kết khách?", + "disableLinkDescription": "Những người xem SSH ẩn danh sẽ bị ngắt kết nối ngay lập tức và liên kết hiện tại sẽ ngừng hoạt động.", + "linkCopied": "Liên kết đã được sao chép", + "roles": "Vai trò", + "users": "Người dùng", + "searchUsers": "Tìm kiếm người dùng", + "endConfirmTitle": "Kết thúc cuộc họp này?", + "endDescription": "{{name}} sẽ kết thúc đối với tất cả mọi người và không thể mở lại.", + "endPersistentDescription": "Buổi thuyết trình đang diễn ra tại {{name}} sẽ dừng lại. Phòng vẫn có thể được sử dụng lại.", + "deleteRoom": "Xóa phòng", + "deleteConfirmTitle": "Xóa phòng này?", + "deleteDescription": "{{name}} sẽ bị xóa vĩnh viễn đối với tất cả mọi người và không thể hoàn tác.", + "invitedTo": "Bạn được mời đến \"{{name}}\"", + "guest": { + "title": "Cuộc họp", + "waiting": "Đang chờ người dẫn chương trình...", + "linkInvalid": "Liên kết dành cho khách này không hợp lệ hoặc cuộc họp đã kết thúc." + }, + "reopenFromPanel": "Tab cuộc họp này đã hết hạn. Vui lòng mở lại phòng từ mục Cuộc họp.", + "requestControl": "Yêu cầu kiểm soát", + "cancelControlRequest": "Yêu cầu hủy bỏ", + "controlQueue": "Yêu cầu kiểm soát", + "noControlRequests": "Không có yêu cầu nào đang chờ xử lý.", + "grantControlTo": "Trao quyền điều khiển cho {{name}}", + "dismissControlRequest": "Bỏ qua yêu cầu điều khiển của {{name}}", + "releaseControl": "Kiểm soát giải phóng", + "grantControl": "Trao quyền kiểm soát", + "revokeControl": "Giành lại quyền kiểm soát", + "controlRequestedBy": "{{name}} yêu cầu quyền điều khiển", + "grant": "Khoản trợ cấp", + "controlBadge": "Trong tầm kiểm soát", + "youArePresenting": "Bạn đang thuyết trình ở một tab khác. Hãy dừng thuyết trình để nhường sân khấu.", + "created": "Phòng được tạo", + "invited": "Các thành viên được mời", + "roomTab": "Cuộc họp" + }, + "nav": { + "collab": "Các cuộc họp" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "Cho phép chia sẻ phiên", + "description": "Cho phép chia sẻ các phiên terminal trực tiếp, RDP, VNC và Telnet trên toàn bộ máy chủ. Khi bị vô hiệu hóa, tùy chọn này sẽ ghi đè lên mọi tùy chọn chia sẻ trên từng máy chủ riêng lẻ." + } + }, + "host": { + "allowSessionSharing": { + "label": "Cho phép chia sẻ phiên", + "description": "Cho phép chia sẻ các phiên trực tiếp trên máy chủ này qua liên kết hoặc với những người dùng khác." + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/zh_CN.json b/plugins/session-sharing/locales/translated/zh_CN.json new file mode 100644 index 000000000..84f9bec59 --- /dev/null +++ b/plugins/session-sharing/locales/translated/zh_CN.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "正在连接到共享会话...", + "linkInvalid": "此共享链接无效、已过期或已被撤销。", + "rateLimited": "尝试次数过多,请稍后再试。", + "sessionEnded": "此会话已结束", + "readOnlyBadge": "仅查看", + "ownerLabel": "房东" + }, + "modalTitle": "共享会话", + "shareButton": "共享", + "notReadyToShare": "会话尚未准备好共享", + "modeTab": { + "link": "链接", + "user": "用户" + }, + "linkModeDescription": "任何人只要拥有此链接即可加入,无需注册账号。", + "userModeDescription": "将主机共享给已拥有此主机访问权限的特定用户。如果他们还没有访问权限,请先将主机共享给他们,或者使用链接代替。共享后,会话将显示在他们的“连接”标签页中。", + "permissionLevel": { + "label": "权限级别", + "readOnly": "只读", + "readOnlyDescription": "可以观看直播,但无法打字或互动。", + "readWrite": "读写", + "readWriteDescription": "可以像会话所有者一样进行输入和互动。" + }, + "expiryLabel": "链接过期", + "createLinkButton": "创建链接", + "createShareButton": "与用户共享", + "searchUsersPlaceholder": "搜索用户...", + "noUsersFound": "未找到用户", + "linkCreated": "已创建共享链接", + "linkCopied": "链接已复制到剪贴板", + "copyLink": "复制链接", + "shareCreated": "会话已共享。它将出现在对方的“连接”标签页中。", + "shareFailed": "创建共享失败", + "userLacksHostAccess": "该用户目前没有访问此主机的权限。请先与其共享主机,或使用链接进行共享。", + "activeShares": "有效共享", + "noActiveShares": "此会话暂无活跃共享", + "revoke": "撤销", + "revokeConfirmTitle": "撤销此共享?", + "revokeConfirmDescription": "任何使用此共享的用户都将立即失去访问权限。", + "revoked": "共享已撤销", + "revokeFailed": "撤销共享失败", + "joinCount": "{{count}} 人加入", + "joinCount_other": "{{count}} 人加入", + "expiresAt": "过期时间 {{date}}", + "linkShareBadge": "链接", + "userShareBadge": "用户:{{username}}", + "loadSharesFailed": "无法加载共享", + "customHoursPlaceholder": "过期前的小时数" + }, + "collab": { + "createRoom": "新房间", + "roomName": "房间名称", + "persistentRoom": "持久房间", + "persistentRoomHint": "会议结束后,持久会议室仍会保留在列表中,可以重复使用。", + "noRooms": "暂无会议室。创建会议室即可开始会议。", + "openRoom": "打开", + "endRoom": "会议结束", + "leaveRoom": "离开", + "deleteMember": "消除", + "removeMember": "移除 {{name}}", + "removeMemberTitle": "移除此成员?", + "removeMemberDescription": "{{name}} 将立即失去对该房间的访问权限。所有正在使用的 SSH 查看器都将被断开连接。", + "invite": "邀请", + "inviteTitle": "邀请成员", + "members": "成员", + "membersPanel": "房间成员和控制请求", + "membersWithCount": "成员({{count}})", + "online": "在线的", + "offline": "离线", + "you": "(你)", + "hostBadge": "主持人", + "presenterBadge": "呈现", + "present": "展示", + "presentTitle": "登上舞台", + "presentHost": "主持人", + "searchHosts": "搜索主机", + "noHostsFound": "未找到可用主机。", + "presentProtocol": "协议", + "stopPresenting": "停止演示", + "takeOver": "接管", + "takeOverConfirmTitle": "替换当前演示文稿?", + "takeOverDescription": "{{name}} 正在直播。接管直播将会断开所有人与该直播间的连接。", + "emptyStage": "目前无人发言。请上台分享您的演讲内容。", + "stageLoading": "连接到舞台……", + "stageEnded": "演讲结束了", + "roomEnded": "会议结束", + "presenterLabel": "{{name}} 正在呈现", + "guestLink": "访客链接", + "guestLinkOn": "访客链接已开启。任何拥有该链接的人都可以观看舞台表演。", + "guestLinkOff": "访客链接已关闭。", + "copyLink": "复制链接", + "rotateLink": "旋转链接", + "disableLink": "禁用链接", + "rotateLinkConfirmTitle": "旋转访客链接?", + "rotateLinkDescription": "当前连接将立即停止工作。已连接的SSH访客将被断开连接。", + "disableLinkConfirmTitle": "禁用访客链接?", + "disableLinkDescription": "匿名 SSH 查看者将被立即断开连接,当前链接将停止工作。", + "linkCopied": "链接已复制", + "roles": "角色", + "users": "用户", + "searchUsers": "搜索用户", + "endConfirmTitle": "结束会议吗?", + "endDescription": "{{name}} 将对所有人关闭,且无法重新开启。", + "endPersistentDescription": "{{name}} 中的当前演示将停止。该房间仍可供再次使用。", + "deleteRoom": "删除房间", + "deleteConfirmTitle": "删除这个房间?", + "deleteDescription": "{{name}} 将被永久删除,且无法撤销。", + "invitedTo": "您受邀参加“{{name}}”", + "guest": { + "title": "会议", + "waiting": "等待主持人……", + "linkInvalid": "此访客链接无效或会议已结束" + }, + "reopenFromPanel": "此会议标签页已过期。请从“会议”面板重新打开会议室。", + "requestControl": "请求控制", + "cancelControlRequest": "取消请求", + "controlQueue": "控制请求", + "noControlRequests": "没有待处理的请求。", + "grantControlTo": "将控制权交给 {{name}}", + "dismissControlRequest": "驳回 {{name}}的控制请求", + "releaseControl": "发布控制", + "grantControl": "赋予控制权", + "revokeControl": "夺回控制权", + "controlRequestedBy": "{{name}} 请求控制权", + "grant": "授予", + "controlBadge": "掌控之中", + "youArePresenting": "您正在另一个标签页中进行演示。请停止演示,将演示权交给对方。", + "created": "创建房间", + "invited": "受邀成员", + "roomTab": "会议" + }, + "nav": { + "collab": "会议" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "允许会话共享", + "description": "允许在整个实例范围内共享实时终端、RDP、VNC 和 Telnet 会话。禁用此功能将覆盖所有主机级别的共享设置。" + } + }, + "host": { + "allowSessionSharing": { + "label": "允许会话共享", + "description": "允许通过链接或其他方式共享此主机上的实时会话。" + } + } + } +} diff --git a/plugins/session-sharing/locales/translated/zh_TW.json b/plugins/session-sharing/locales/translated/zh_TW.json new file mode 100644 index 000000000..03a23b6a6 --- /dev/null +++ b/plugins/session-sharing/locales/translated/zh_TW.json @@ -0,0 +1,152 @@ +{ + "sessionSharing": { + "guestView": { + "loading": "正在連接到共享會話...", + "linkInvalid": "此分享連結無效、已過期或已撤銷。", + "rateLimited": "嘗試次數過多,請稍後再試。", + "sessionEnded": "本次會議已結束", + "readOnlyBadge": "僅查看", + "ownerLabel": "房東" + }, + "modalTitle": "分享會話", + "shareButton": "分享", + "notReadyToShare": "會話尚未準備好分享", + "modeTab": { + "link": "關聯", + "user": "使用者" + }, + "linkModeDescription": "任何人只要擁有此連結即可加入,無需註冊帳號。", + "userModeDescription": "將主機共用給已擁有此主機存取權限的特定使用者。如果他們還沒有存取權限,請先將主機分享給他們,或使用連結代替。共享後,會話將顯示在他們的“連接”選項卡中。", + "permissionLevel": { + "label": "權限等級", + "readOnly": "只讀", + "readOnlyDescription": "可以觀看直播,但無法打字或互動。", + "readWrite": "讀寫", + "readWriteDescription": "可以像會話擁有者一樣進行輸入和互動。" + }, + "expiryLabel": "連結過期", + "createLinkButton": "建立連結", + "createShareButton": "與用戶分享", + "searchUsersPlaceholder": "搜尋用戶...", + "noUsersFound": "未找到用戶", + "linkCreated": "已建立分享連結", + "linkCopied": "連結已複製到剪貼簿", + "copyLink": "複製連結", + "shareCreated": "會話已共用。它將出現在對方的「連線」標籤中。", + "shareFailed": "建立共享失敗", + "userLacksHostAccess": "該使用者目前沒有存取此主機的權限。請先與其共享主機,或使用連結進行共享。", + "activeShares": "活躍股", + "noActiveShares": "本會話無活躍份額", + "revoke": "撤銷", + "revokeConfirmTitle": "撤銷此份額?", + "revokeConfirmDescription": "任何使用此共用資料夾的使用者都將立即失去存取權限。", + "revoked": "股份撤銷", + "revokeFailed": "撤銷股份失敗", + "joinCount": "{{count}} 加入", + "joinCount_other": "{{count}} 加入", + "expiresAt": "過期時間 {{date}}", + "linkShareBadge": "關聯", + "userShareBadge": "使用者: {{username}}", + "loadSharesFailed": "無法加載活動份額", + "customHoursPlaceholder": "存取到期前的小時數" + }, + "collab": { + "createRoom": "新房間", + "roomName": "房間名稱", + "persistentRoom": "持久房間", + "persistentRoomHint": "會議結束後,持久會議室仍會保留在清單中,可重複使用。", + "noRooms": "暫無會議室。創建會議室即可開始會議。", + "openRoom": "打開", + "endRoom": "會議結束", + "leaveRoom": "離開", + "deleteMember": "消除", + "removeMember": "移除 {{name}}", + "removeMemberTitle": "移除此成員?", + "removeMemberDescription": "{{name}} 將立即失去該房間的存取權限。所有正在使用的 SSH 檢視器都將被斷開。", + "invite": "邀請", + "inviteTitle": "邀請成員", + "members": "成員", + "membersPanel": "房間成員和控制請求", + "membersWithCount": "成員({{count}})", + "online": "在線的", + "offline": "離線", + "you": "(你)", + "hostBadge": "主持人", + "presenterBadge": "呈現", + "present": "展示", + "presentTitle": "登上舞台", + "presentHost": "主持人", + "searchHosts": "搜尋主機", + "noHostsFound": "未找到可用主機。", + "presentProtocol": "協定", + "stopPresenting": "停止演示", + "takeOver": "接管", + "takeOverConfirmTitle": "替換當前簡報?", + "takeOverDescription": "{{name}} 正在直播中。接管直播將會中斷所有人與該直播間的連結。", + "emptyStage": "目前無人發言。請上台分享您的演講內容。", + "stageLoading": "連接到舞台…", + "stageEnded": "演講結束了", + "roomEnded": "會議結束", + "presenterLabel": "{{name}} 正在呈現", + "guestLink": "訪客連結", + "guestLinkOn": "訪客連結已開啟。任何擁有該連結的人都可以觀看舞台表演。", + "guestLinkOff": "訪客連結已關閉。", + "copyLink": "複製連結", + "rotateLink": "旋轉連結", + "disableLink": "停用連結", + "rotateLinkConfirmTitle": "旋轉訪客連結?", + "rotateLinkDescription": "當前連線將立即停止工作。已連線的SSH訪客將會被斷開連線。", + "disableLinkConfirmTitle": "禁用訪客連結?", + "disableLinkDescription": "匿名 SSH 檢視者將立即被斷開連接,目前連結將停止運作。", + "linkCopied": "連結已複製", + "roles": "角色", + "users": "使用者", + "searchUsers": "搜尋用戶", + "endConfirmTitle": "結束會議嗎?", + "endDescription": "{{name}} 將對所有人關閉,且無法重新開啟。", + "endPersistentDescription": "{{name}} 中的目前示範將停止。該房間仍可供再次使用。", + "deleteRoom": "刪除房間", + "deleteConfirmTitle": "刪除這個房間?", + "deleteDescription": "{{name}} 將會永久刪除,且無法撤銷。", + "invitedTo": "您受邀參加“{{name}}”", + "guest": { + "title": "會議", + "waiting": "等待主持人…", + "linkInvalid": "此訪客連結無效或會議已結束" + }, + "reopenFromPanel": "此會議標籤頁已過期。請從「會議」面板重新開啟會議室。", + "requestControl": "請求控制", + "cancelControlRequest": "取消請求", + "controlQueue": "控制請求", + "noControlRequests": "沒有待處理的請求。", + "grantControlTo": "將控制權交給 {{name}}", + "dismissControlRequest": "駁回 {{name}}的控制請求", + "releaseControl": "發布控制", + "grantControl": "賦予控制權", + "revokeControl": "奪回控制權", + "controlRequestedBy": "{{name}} 請求控制權", + "grant": "授予", + "controlBadge": "掌控之中", + "youArePresenting": "您正在另一個標籤頁中進行示範。請停止演示,將演示權交給對方。", + "created": "創建房間", + "invited": "受邀成員", + "roomTab": "會議" + }, + "nav": { + "collab": "會議" + }, + "settings": { + "admin": { + "globallyEnabled": { + "label": "允許會話共享", + "description": "允許在整個執行個體範圍內共用即時終端、RDP、VNC 和 Telnet 會話。停用此功能將覆蓋所有主機層級的共用設定。" + } + }, + "host": { + "allowSessionSharing": { + "label": "允許會話共享", + "description": "允許透過連結或其他方式分享此主機上的即時會話。" + } + } + } +} diff --git a/plugins/session-sharing/manifest.json b/plugins/session-sharing/manifest.json new file mode 100644 index 000000000..e9f67d33c --- /dev/null +++ b/plugins/session-sharing/manifest.json @@ -0,0 +1,101 @@ +{ + "id": "session-sharing", + "name": "Session Sharing", + "version": "1.0.0", + "description": "Share a live terminal or remote desktop session by link or with another user, and present sessions to a group in collaboration rooms.", + "author": { + "name": "Termix" + }, + "license": "MIT", + "repository": "https://github.com/Termix-SSH/Termix", + "category": "Productivity", + "icon": "Presentation", + "engine": { + "termix": ">=2.9.0", + "api": "1" + }, + "capabilities": ["hosts:read", "db:own", "network:serve", "ui:surface"], + "dependencies": { + "ssh-terminal": "^1.0.0" + }, + "optionalDependencies": { + "remote-desktop": "^1.0.0" + }, + "provides": [ + { + "service": "sessions.sharing", + "version": "1.0.0", + "permission": "session-sharing.use" + } + ], + "requires": [ + { + "service": "sessions.live", + "versionRange": "^1.0.0" + } + ], + "contributes": { + "guest": true, + "guestViews": ["shared", "collab-guest"], + "tabs": [ + { + "id": "collab", + "titleKey": "nav.collab", + "icon": "Presentation", + "openFrom": ["rail"] + } + ], + "actions": [ + { + "id": "session-sharing.share", + "titleKey": "sessionSharing.shareButton", + "handler": "share", + "permission": "session-sharing.use" + }, + { + "id": "sessions.sharedWithMe", + "titleKey": "sessionSharing.sharedWithMe", + "handler": "sharedWithMe", + "permission": "session-sharing.use" + } + ], + "actionSlots": [ + { + "id": "session.remoteDisplay", + "accepts": ["component"] + } + ], + "permissions": [ + { + "name": "use", + "titleKey": "permissions.use.title", + "descriptionKey": "permissions.use.description", + "defaultRoles": ["admin", "user"] + } + ], + "settings": { + "admin": [ + { + "key": "globallyEnabled", + "type": "boolean", + "labelKey": "settings.admin.globallyEnabled.label", + "descriptionKey": "settings.admin.globallyEnabled.description", + "default": true + } + ], + "host": { + "fields": [ + { + "key": "allowSessionSharing", + "type": "boolean", + "labelKey": "settings.host.allowSessionSharing.label", + "descriptionKey": "settings.host.allowSessionSharing.description", + "default": true + } + ] + } + } + }, + "locales": "locales", + "platforms": ["linux", "win32", "darwin"] +} diff --git a/plugins/session-sharing/migrations/mysql/0001_adopt_session_sharing.sql b/plugins/session-sharing/migrations/mysql/0001_adopt_session_sharing.sql new file mode 100644 index 000000000..5a77df38e --- /dev/null +++ b/plugins/session-sharing/migrations/mysql/0001_adopt_session_sharing.sql @@ -0,0 +1,67 @@ +-- session-sharing 0001: adopt_session_sharing +-- Generated by termix-plugin migrations. Review before committing. +-- Hand-edited: keeps the legacy foreign keys the SDK cannot declare +-- (nullable or set-null user and host links, and links between these tables). + +CREATE TABLE IF NOT EXISTS `session_shares` ( + `id` varchar(255) PRIMARY KEY NOT NULL, + `host_id` int NOT NULL, + `owner_user_id` varchar(255) NOT NULL, + `protocol` text NOT NULL, + `session_id` varchar(255) NOT NULL, + `tab_instance_id` text, + `share_type` text NOT NULL, + `target_user_id` varchar(255), + `link_token` varchar(255) UNIQUE, + `permission_level` text NOT NULL DEFAULT ('read-only'), + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `expires_at` text NOT NULL, + `revoked_at` text, + `last_joined_at` text, + `join_count` int NOT NULL DEFAULT 0, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`owner_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`target_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +); +RENAME TABLE `session_shares` TO `p_session_sharing_shares`; +CREATE TABLE IF NOT EXISTS `session_share_participants` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `share_id` varchar(255) NOT NULL, + `user_id` varchar(255), + `guest_label` text, + `joined_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `left_at` text, + FOREIGN KEY (`share_id`) REFERENCES `p_session_sharing_shares` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +); +RENAME TABLE `session_share_participants` TO `p_session_sharing_share_participants`; +CREATE TABLE IF NOT EXISTS `collab_rooms` ( + `id` varchar(255) PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `owner_user_id` varchar(255) NOT NULL, + `persistent` boolean NOT NULL DEFAULT false, + `presenter_user_id` varchar(255), + `stage_protocol` text, + `stage_host_id` int, + `stage_share_id` varchar(255), + `guest_link_token` varchar(255), + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `ended_at` text, + FOREIGN KEY (`owner_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`presenter_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL, + FOREIGN KEY (`stage_host_id`) REFERENCES `ssh_data` (`id`) ON DELETE SET NULL, + FOREIGN KEY (`stage_share_id`) REFERENCES `p_session_sharing_shares` (`id`) ON DELETE SET NULL +); +RENAME TABLE `collab_rooms` TO `p_session_sharing_rooms`; +CREATE TABLE IF NOT EXISTS `collab_room_members` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `room_id` varchar(255) NOT NULL, + `user_id` varchar(255) NOT NULL, + `room_role` text NOT NULL DEFAULT ('member'), + `added_by` varchar(255), + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`room_id`) REFERENCES `p_session_sharing_rooms` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`added_by`) REFERENCES `users` (`id`) ON DELETE SET NULL +); +RENAME TABLE `collab_room_members` TO `p_session_sharing_room_members`; diff --git a/plugins/session-sharing/migrations/postgres/0001_adopt_session_sharing.sql b/plugins/session-sharing/migrations/postgres/0001_adopt_session_sharing.sql new file mode 100644 index 000000000..038407e9e --- /dev/null +++ b/plugins/session-sharing/migrations/postgres/0001_adopt_session_sharing.sql @@ -0,0 +1,73 @@ +-- session-sharing 0001: adopt_session_sharing +-- Generated by termix-plugin migrations. Review before committing. +-- Hand-edited: keeps the legacy foreign keys the SDK cannot declare +-- (nullable or set-null user and host links, and links between these tables). + +CREATE TABLE IF NOT EXISTS "session_shares" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "host_id" integer NOT NULL, + "owner_user_id" varchar(255) NOT NULL, + "protocol" text NOT NULL, + "session_id" varchar(255) NOT NULL, + "tab_instance_id" text, + "share_type" text NOT NULL, + "target_user_id" varchar(255), + "link_token" varchar(255) UNIQUE, + "permission_level" text NOT NULL DEFAULT 'read-only', + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expires_at" text NOT NULL, + "revoked_at" text, + "last_joined_at" text, + "join_count" integer NOT NULL DEFAULT 0, + FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE, + FOREIGN KEY ("owner_user_id") REFERENCES "users" ("id") ON DELETE CASCADE, + FOREIGN KEY ("target_user_id") REFERENCES "users" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_session_shares_session_id" ON "session_shares" ("session_id"); +CREATE INDEX IF NOT EXISTS "idx_session_shares_host_id" ON "session_shares" ("host_id"); +ALTER TABLE "session_shares" RENAME TO "p_session_sharing_shares"; +CREATE TABLE IF NOT EXISTS "session_share_participants" ( + "id" serial PRIMARY KEY, + "share_id" varchar(255) NOT NULL, + "user_id" varchar(255), + "guest_label" text, + "joined_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "left_at" text, + FOREIGN KEY ("share_id") REFERENCES "p_session_sharing_shares" ("id") ON DELETE CASCADE, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE +); +ALTER TABLE "session_share_participants" RENAME TO "p_session_sharing_share_participants"; +CREATE TABLE IF NOT EXISTS "collab_rooms" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "owner_user_id" varchar(255) NOT NULL, + "persistent" boolean NOT NULL DEFAULT false, + "presenter_user_id" varchar(255), + "stage_protocol" text, + "stage_host_id" integer, + "stage_share_id" varchar(255), + "guest_link_token" varchar(255), + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "ended_at" text, + FOREIGN KEY ("owner_user_id") REFERENCES "users" ("id") ON DELETE CASCADE, + FOREIGN KEY ("presenter_user_id") REFERENCES "users" ("id") ON DELETE SET NULL, + FOREIGN KEY ("stage_host_id") REFERENCES "ssh_data" ("id") ON DELETE SET NULL, + FOREIGN KEY ("stage_share_id") REFERENCES "p_session_sharing_shares" ("id") ON DELETE SET NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_collab_rooms_guest_token" ON "collab_rooms" ("guest_link_token"); +CREATE INDEX IF NOT EXISTS "idx_collab_rooms_owner" ON "collab_rooms" ("owner_user_id"); +ALTER TABLE "collab_rooms" RENAME TO "p_session_sharing_rooms"; +CREATE TABLE IF NOT EXISTS "collab_room_members" ( + "id" serial PRIMARY KEY, + "room_id" varchar(255) NOT NULL, + "user_id" varchar(255) NOT NULL, + "room_role" text NOT NULL DEFAULT 'member', + "added_by" varchar(255), + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE, + FOREIGN KEY ("room_id") REFERENCES "p_session_sharing_rooms" ("id") ON DELETE CASCADE, + FOREIGN KEY ("added_by") REFERENCES "users" ("id") ON DELETE SET NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_collab_room_members_room_user" ON "collab_room_members" ("room_id", "user_id"); +CREATE INDEX IF NOT EXISTS "idx_collab_room_members_user" ON "collab_room_members" ("user_id"); +ALTER TABLE "collab_room_members" RENAME TO "p_session_sharing_room_members"; diff --git a/plugins/session-sharing/migrations/snapshot.json b/plugins/session-sharing/migrations/snapshot.json new file mode 100644 index 000000000..4341c1fa0 --- /dev/null +++ b/plugins/session-sharing/migrations/snapshot.json @@ -0,0 +1,223 @@ +{ + "version": 1, + "tables": { + "shares": { + "columns": { + "id": { + "type": "varchar", + "length": 255, + "primaryKey": true, + "notNull": true + }, + "hostId": { + "type": "refHost", + "notNull": true + }, + "ownerUserId": { + "type": "refUser", + "length": 255, + "notNull": true + }, + "protocol": { + "type": "text", + "notNull": true + }, + "sessionId": { + "type": "varchar", + "length": 255, + "notNull": true + }, + "tabInstanceId": { + "type": "text" + }, + "shareType": { + "type": "text", + "notNull": true + }, + "targetUserId": { + "type": "varchar", + "length": 255 + }, + "linkToken": { + "type": "varchar", + "length": 255, + "unique": true + }, + "permissionLevel": { + "type": "text", + "notNull": true, + "defaultValue": "read-only" + }, + "createdAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + }, + "expiresAt": { + "type": "text", + "notNull": true + }, + "revokedAt": { + "type": "text" + }, + "lastJoinedAt": { + "type": "text" + }, + "joinCount": { + "type": "integer", + "notNull": true, + "defaultValue": 0 + } + }, + "indexes": [ + { + "name": "idx_session_shares_session_id", + "columns": ["sessionId"] + }, + { + "name": "idx_session_shares_host_id", + "columns": ["hostId"] + } + ], + "adopts": "session_shares" + }, + "share_participants": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "shareId": { + "type": "varchar", + "length": 255, + "notNull": true + }, + "userId": { + "type": "varchar", + "length": 255 + }, + "guestLabel": { + "type": "text" + }, + "joinedAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + }, + "leftAt": { + "type": "text" + } + }, + "indexes": [], + "adopts": "session_share_participants" + }, + "rooms": { + "columns": { + "id": { + "type": "varchar", + "length": 255, + "primaryKey": true, + "notNull": true + }, + "name": { + "type": "text", + "notNull": true + }, + "ownerUserId": { + "type": "refUser", + "length": 255, + "notNull": true + }, + "persistent": { + "type": "boolean", + "notNull": true, + "defaultValue": false + }, + "presenterUserId": { + "type": "varchar", + "length": 255 + }, + "stageProtocol": { + "type": "text" + }, + "stageHostId": { + "type": "integer" + }, + "stageShareId": { + "type": "varchar", + "length": 255 + }, + "guestLinkToken": { + "type": "varchar", + "length": 255 + }, + "createdAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + }, + "endedAt": { + "type": "text" + } + }, + "indexes": [ + { + "name": "idx_collab_rooms_guest_token", + "columns": ["guestLinkToken"], + "unique": true + }, + { + "name": "idx_collab_rooms_owner", + "columns": ["ownerUserId"] + } + ], + "adopts": "collab_rooms" + }, + "room_members": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "roomId": { + "type": "varchar", + "length": 255, + "notNull": true + }, + "userId": { + "type": "refUser", + "length": 255, + "notNull": true + }, + "roomRole": { + "type": "text", + "notNull": true, + "defaultValue": "member" + }, + "addedBy": { + "type": "varchar", + "length": 255 + }, + "createdAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + } + }, + "indexes": [ + { + "name": "idx_collab_room_members_room_user", + "columns": ["roomId", "userId"], + "unique": true + }, + { + "name": "idx_collab_room_members_user", + "columns": ["userId"] + } + ], + "adopts": "collab_room_members" + } + } +} diff --git a/plugins/session-sharing/migrations/sqlite/0001_adopt_session_sharing.sql b/plugins/session-sharing/migrations/sqlite/0001_adopt_session_sharing.sql new file mode 100644 index 000000000..45947f678 --- /dev/null +++ b/plugins/session-sharing/migrations/sqlite/0001_adopt_session_sharing.sql @@ -0,0 +1,73 @@ +-- session-sharing 0001: adopt_session_sharing +-- Generated by termix-plugin migrations. Review before committing. +-- Hand-edited: keeps the legacy foreign keys the SDK cannot declare +-- (nullable or set-null user and host links, and links between these tables). + +CREATE TABLE IF NOT EXISTS "session_shares" ( + "id" text PRIMARY KEY NOT NULL, + "host_id" integer NOT NULL, + "owner_user_id" text NOT NULL, + "protocol" text NOT NULL, + "session_id" text NOT NULL, + "tab_instance_id" text, + "share_type" text NOT NULL, + "target_user_id" text, + "link_token" text UNIQUE, + "permission_level" text NOT NULL DEFAULT 'read-only', + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expires_at" text NOT NULL, + "revoked_at" text, + "last_joined_at" text, + "join_count" integer NOT NULL DEFAULT 0, + FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE, + FOREIGN KEY ("owner_user_id") REFERENCES "users" ("id") ON DELETE CASCADE, + FOREIGN KEY ("target_user_id") REFERENCES "users" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_session_shares_session_id" ON "session_shares" ("session_id"); +CREATE INDEX IF NOT EXISTS "idx_session_shares_host_id" ON "session_shares" ("host_id"); +ALTER TABLE "session_shares" RENAME TO "p_session_sharing_shares"; +CREATE TABLE IF NOT EXISTS "session_share_participants" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "share_id" text NOT NULL, + "user_id" text, + "guest_label" text, + "joined_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "left_at" text, + FOREIGN KEY ("share_id") REFERENCES "p_session_sharing_shares" ("id") ON DELETE CASCADE, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE +); +ALTER TABLE "session_share_participants" RENAME TO "p_session_sharing_share_participants"; +CREATE TABLE IF NOT EXISTS "collab_rooms" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "owner_user_id" text NOT NULL, + "persistent" integer NOT NULL DEFAULT 0, + "presenter_user_id" text, + "stage_protocol" text, + "stage_host_id" integer, + "stage_share_id" text, + "guest_link_token" text, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "ended_at" text, + FOREIGN KEY ("owner_user_id") REFERENCES "users" ("id") ON DELETE CASCADE, + FOREIGN KEY ("presenter_user_id") REFERENCES "users" ("id") ON DELETE SET NULL, + FOREIGN KEY ("stage_host_id") REFERENCES "ssh_data" ("id") ON DELETE SET NULL, + FOREIGN KEY ("stage_share_id") REFERENCES "p_session_sharing_shares" ("id") ON DELETE SET NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_collab_rooms_guest_token" ON "collab_rooms" ("guest_link_token"); +CREATE INDEX IF NOT EXISTS "idx_collab_rooms_owner" ON "collab_rooms" ("owner_user_id"); +ALTER TABLE "collab_rooms" RENAME TO "p_session_sharing_rooms"; +CREATE TABLE IF NOT EXISTS "collab_room_members" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "room_id" text NOT NULL, + "user_id" text NOT NULL, + "room_role" text NOT NULL DEFAULT 'member', + "added_by" text, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE, + FOREIGN KEY ("room_id") REFERENCES "p_session_sharing_rooms" ("id") ON DELETE CASCADE, + FOREIGN KEY ("added_by") REFERENCES "users" ("id") ON DELETE SET NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_collab_room_members_room_user" ON "collab_room_members" ("room_id", "user_id"); +CREATE INDEX IF NOT EXISTS "idx_collab_room_members_user" ON "collab_room_members" ("user_id"); +ALTER TABLE "collab_room_members" RENAME TO "p_session_sharing_room_members"; diff --git a/plugins/session-sharing/package.json b/plugins/session-sharing/package.json new file mode 100644 index 000000000..860f834c5 --- /dev/null +++ b/plugins/session-sharing/package.json @@ -0,0 +1,21 @@ +{ + "name": "@termix-plugin/session-sharing", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "build": "termix-plugin build", + "test": "termix-plugin test", + "typecheck": "tsc -p tsconfig.json", + "validate": "termix-plugin validate" + }, + "dependencies": { + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "react-xtermjs": "^1.0.10", + "redis": "^6.2.1" + }, + "devDependencies": { + "@termix/plugin-sdk": "*" + } +} diff --git a/plugins/session-sharing/src/backend/deps.ts b/plugins/session-sharing/src/backend/deps.ts new file mode 100644 index 000000000..d05d347e9 --- /dev/null +++ b/plugins/session-sharing/src/backend/deps.ts @@ -0,0 +1,51 @@ +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import type { LiveSessions } from "./live.js"; +import type { RateLimiter } from "./rate-limit.js"; +import type { + Directory, + RoomRepository, + ShareRepository, +} from "./repositories.js"; +import type { CollabRoomHub } from "./room-hub.js"; +import type { CollabRuntimeStore } from "./runtime-store.js"; + +/** Everything the routes and services share, built once in activate. */ +export interface SharingDeps { + ctx: PluginContext; + shares: ShareRepository; + rooms: RoomRepository; + directory: Directory; + live: LiveSessions; + hub: CollabRoomHub; + store: CollabRuntimeStore; + /** /resolve/:linkToken, 30 a minute per IP. */ + resolveLimiter: RateLimiter; + /** Room guest links, 60 a minute per IP. */ + guestLimiter: RateLimiter; + isSharingEnabledForHost: (hostId: number) => Promise; +} + +export const PROTOCOLS = ["ssh", "rdp", "vnc", "telnet"] as const; + +/** Where SSH shares are joined: the terminal's own socket. */ +export const TERMINAL_WS_PATH = "/plugin-ws/ssh-terminal/terminal"; +/** Where remote desktop viewers connect. */ +export const DISPLAY_WS_PATH = "/plugin-ws/remote-desktop/display"; + +export function clientIp(req: { + ip?: string; + socket?: { remoteAddress?: string }; +}): string { + return req.ip || req.socket?.remoteAddress || "unknown"; +} + +export function isNonEmptyString(value: unknown): value is string { + return typeof value === "string" && value.trim().length > 0; +} + +/** The acting user, which core set from the authenticated request. */ +export function actorOf(ctx: PluginContext): string { + const userId = ctx.currentActor(); + if (!userId) throw new Error("No acting user"); + return userId; +} diff --git a/plugins/session-sharing/src/backend/index.ts b/plugins/session-sharing/src/backend/index.ts new file mode 100644 index 000000000..37f012c8c --- /dev/null +++ b/plugins/session-sharing/src/backend/index.ts @@ -0,0 +1,80 @@ +import type { Router } from "express"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import type { SharingDeps } from "./deps.js"; +import { createLiveSessions, isSharingEnabledForHost } from "./live.js"; +import { createRateLimiter } from "./rate-limit.js"; +import { + createDirectory, + createRoomRepository, + createShareRepository, + type CoreRefs, + type Tables, +} from "./repositories.js"; +import { CollabRoomHub } from "./room-hub.js"; +import { createRoomActions, registerRoomRoutes } from "./room-routes.js"; +import { CollabRuntimeStore } from "./runtime-store.js"; +import { + SESSION_GUESTS_KEY, + createSessionGuests, + createSessionSharingService, +} from "./services.js"; +import { registerShareRoutes } from "./share-routes.js"; +import { roomMembers, rooms, shareParticipants, shares } from "./tables.js"; + +export async function activate(ctx: PluginContext) { + const tables: Tables = { + shares: await ctx.db.define(shares), + participants: await ctx.db.define(shareParticipants), + rooms: await ctx.db.define(rooms), + members: await ctx.db.define(roomMembers), + }; + const refs = () => ctx.db.refs(); + + const directory = createDirectory(ctx.db, refs); + const store = new CollabRuntimeStore(ctx.log); + const hub = new CollabRoomHub(store); + const resolveLimiter = createRateLimiter({ + windowMs: 60_000, + maxAttempts: 30, + }); + const guestLimiter = createRateLimiter({ windowMs: 60_000, maxAttempts: 60 }); + ctx.disposables.add(() => { + hub.dispose(); + resolveLimiter.dispose(); + guestLimiter.dispose(); + return store.close(); + }); + + const deps: SharingDeps = { + ctx, + shares: createShareRepository(ctx.db, tables), + rooms: createRoomRepository(ctx.db, tables, refs), + directory, + live: createLiveSessions(ctx), + hub, + store, + resolveLimiter, + guestLimiter, + isSharingEnabledForHost: (hostId) => + isSharingEnabledForHost(ctx, directory.hostExists, hostId), + }; + + const actions = createRoomActions(deps); + store.onEvent(actions.onRuntimeEvent); + + // Guest links carry their own token, checked in the handlers. + const router = ctx.http.router({ + public: ["/resolve/:linkToken", "/guest/:token"], + }); + registerShareRoutes(router, deps); + registerRoomRoutes(router, deps, actions); + + ctx.services.provide("sessions.sharing", createSessionSharingService(deps)); + ctx.registry.provide(SESSION_GUESTS_KEY, createSessionGuests(deps)); + + ctx.log.info("Session sharing mounted at /plugin-api/session-sharing"); +} + +export async function deactivate() { + // Everything above was registered through ctx and is disposed by core. +} diff --git a/plugins/session-sharing/src/backend/live.ts b/plugins/session-sharing/src/backend/live.ts new file mode 100644 index 000000000..904b036d3 --- /dev/null +++ b/plugins/session-sharing/src/backend/live.ts @@ -0,0 +1,183 @@ +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import type { LiveProtocol } from "./repositories.js"; + +export const LIVE_SESSIONS_SERVICE = "sessions.live"; + +/** What every sessions.live provider reports about one session. */ +export interface LiveSessionInfo { + id: string; + userId: string; + hostId: number; + hostName: string; + isConnected: boolean; + createdAt: number; + tabInstanceId: string | null; +} + +/** + * sessions.live v1, keyed by session type. ssh-terminal provides "ssh"; + * remote-desktop provides "rdp", "vnc" and "telnet". Through a service handle + * every method answers with a promise. The control methods are the terminal's + * (a remote desktop viewer cannot be kicked or handed control), and + * createViewerToken is remote desktop's: a join token for a viewer. + */ +export interface LiveSessionProvider { + getSession: (sessionId: string) => LiveSessionInfo | null; + ownerEndSession?: (sessionId: string, reason: string) => void; + disconnectParticipants?: ( + sessionId: string, + shareId: string, + options: { reason: string; userId?: string | null }, + ) => number; + setRoomShareControl?: ( + sessionId: string, + shareId: string, + controllerUserId: string | null, + ) => void; + createViewerToken?: (sessionId: string, readOnly: boolean) => string; +} + +type Async = { + [K in keyof T]-?: T[K] extends ((...args: infer A) => infer R) | undefined + ? (...args: A) => Promise + : never; +}; + +export type LiveSessions = ReturnType; + +/** + * Reaches live sessions through sessions.live as the acting user. A caller + * with no actor (a public guest route) passes the share owner, who owns the + * session being looked at. + */ +export function createLiveSessions(ctx: PluginContext) { + async function run( + asUser: string | undefined, + fn: () => Promise, + ): Promise { + if (asUser && ctx.currentActor() !== asUser) { + return ctx.asUser(asUser, fn); + } + return fn(); + } + + function provider(protocol: string): Async | null { + const handle = ctx.services.get>( + LIVE_SESSIONS_SERVICE, + { provider: protocol }, + ); + return "getSession" in handle ? handle : null; + } + + async function getSession( + protocol: string, + sessionId: string, + asUser?: string, + ): Promise { + const live = provider(protocol); + if (!live) return null; + try { + return (await run(asUser, () => live.getSession(sessionId))) ?? null; + } catch { + return null; + } + } + + return { + getSession, + + async isLive( + protocol: string, + sessionId: string, + asUser?: string, + ): Promise { + const session = await getSession(protocol, sessionId, asUser); + return !!session?.isConnected; + }, + + async isOwnedBy( + protocol: LiveProtocol, + sessionId: string, + userId: string, + ): Promise { + const session = await getSession(protocol, sessionId); + return !!session && session.isConnected && session.userId === userId; + }, + + /** Ends the session for every guest. Best effort, like before. */ + async ownerEndSession( + protocol: string, + sessionId: string, + reason: string, + asUser?: string, + ): Promise { + const live = provider(protocol); + if (!live || !("ownerEndSession" in live)) return; + await run(asUser, () => live.ownerEndSession(sessionId, reason)).catch( + () => {}, + ); + }, + + async disconnectParticipants( + protocol: string, + sessionId: string, + shareId: string, + options: { reason: string; userId?: string | null }, + asUser?: string, + ): Promise { + const live = provider(protocol); + if (!live || !("disconnectParticipants" in live)) return; + await run(asUser, () => + live.disconnectParticipants(sessionId, shareId, options), + ).catch(() => {}); + }, + + async setRoomShareControl( + protocol: string, + sessionId: string, + shareId: string, + controllerUserId: string | null, + asUser?: string, + ): Promise { + const live = provider(protocol); + if (!live || !("setRoomShareControl" in live)) return; + await run(asUser, () => + live.setRoomShareControl(sessionId, shareId, controllerUserId), + ).catch(() => {}); + }, + + /** A viewer token for a remote desktop session. Throws while unavailable. */ + async createViewerToken( + protocol: string, + sessionId: string, + readOnly: boolean, + asUser?: string, + ): Promise { + const live = provider(protocol); + if (!live || !("createViewerToken" in live)) { + throw new Error(`No ${protocol} session provider is running`); + } + return run(asUser, () => live.createViewerToken(sessionId, readOnly)); + }, + }; +} + +/** + * Whether sessions on a host may be shared: the admin switch first, then the + * host's own setting. Both default to on. A host that does not exist cannot + * be shared. + */ +export async function isSharingEnabledForHost( + ctx: PluginContext, + hostExists: (hostId: number) => Promise, + hostId: number, +): Promise { + if ((await ctx.settings.get("globallyEnabled")) === false) { + return false; + } + if (!(await hostExists(hostId))) return false; + return ( + (await ctx.settings.getHost(hostId, "allowSessionSharing")) !== + false + ); +} diff --git a/plugins/session-sharing/src/backend/rate-limit.ts b/plugins/session-sharing/src/backend/rate-limit.ts new file mode 100644 index 000000000..2994ba6d3 --- /dev/null +++ b/plugins/session-sharing/src/backend/rate-limit.ts @@ -0,0 +1,37 @@ +/** + * A per-IP fixed window, for the public guest endpoints. `dispose` stops the + * sweep that forgets old windows. + */ +export function createRateLimiter(options: { + windowMs: number; + maxAttempts: number; +}) { + const attempts = new Map(); + const sweep = setInterval(() => { + const now = Date.now(); + for (const [ip, entry] of attempts) { + if (now - entry.windowStart > options.windowMs) attempts.delete(ip); + } + }, 5 * options.windowMs); + sweep.unref(); + + return { + /** Counts an attempt and says whether this IP is over the limit. */ + isLimited(ip: string): boolean { + const now = Date.now(); + const entry = attempts.get(ip); + if (!entry || now - entry.windowStart > options.windowMs) { + attempts.set(ip, { count: 1, windowStart: now }); + return false; + } + entry.count += 1; + return entry.count > options.maxAttempts; + }, + dispose(): void { + clearInterval(sweep); + attempts.clear(); + }, + }; +} + +export type RateLimiter = ReturnType; diff --git a/plugins/session-sharing/src/backend/repositories.ts b/plugins/session-sharing/src/backend/repositories.ts new file mode 100644 index 000000000..2106c9d0a --- /dev/null +++ b/plugins/session-sharing/src/backend/repositories.ts @@ -0,0 +1,580 @@ +import { and, desc, eq, gt, inArray, isNull } from "drizzle-orm"; +import type { PluginDatabase } from "@termix/plugin-sdk/backend"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Tables come from ctx.db.define, which the SDK hands back untyped, and the +// drizzle handle is the server's own. Typed at this module's edge instead. +type Table = any; +type Drizzle = any; +/* eslint-enable @typescript-eslint/no-explicit-any */ + +export type ShareType = "link" | "user" | "room"; +export type PermissionLevel = "read-only" | "read-write"; +export type LiveProtocol = "ssh" | "rdp" | "vnc" | "telnet"; + +export interface ShareRecord { + id: string; + hostId: number; + ownerUserId: string; + protocol: string; + sessionId: string; + tabInstanceId: string | null; + shareType: string; + targetUserId: string | null; + linkToken: string | null; + permissionLevel: string; + createdAt: string; + expiresAt: string; + revokedAt: string | null; + lastJoinedAt: string | null; + joinCount: number; +} + +export interface ShareCreateInput { + id: string; + hostId: number; + ownerUserId: string; + protocol: string; + sessionId: string; + tabInstanceId?: string | null; + shareType: ShareType; + targetUserId?: string | null; + linkToken?: string | null; + permissionLevel: PermissionLevel; + expiresAt: string; +} + +export interface RoomRecord { + id: string; + name: string; + ownerUserId: string; + persistent: boolean; + presenterUserId: string | null; + stageProtocol: string | null; + stageHostId: number | null; + stageShareId: string | null; + guestLinkToken: string | null; + createdAt: string; + endedAt: string | null; +} + +export interface RoomMemberRecord { + id: number; + roomId: string; + userId: string; + roomRole: string; + addedBy: string | null; + createdAt: string; +} + +export interface RoomMemberWithUser { + userId: string; + username: string; + roomRole: string; + createdAt: string; +} + +export interface RoomStage { + presenterUserId: string | null; + stageProtocol: string | null; + stageHostId: number | null; + stageShareId: string | null; +} + +export interface Tables { + shares: Table; + participants: Table; + rooms: Table; + members: Table; +} + +export interface CoreRefs { + users: Table; + hosts: Table; + roles: Table; + userRoles: Table; +} + +const nowIso = () => new Date().toISOString(); + +/** Not revoked and not expired. Expiry is compared as ISO text, as before. */ +function activeShare(shares: Table, now: string) { + return and(isNull(shares.revokedAt), gt(shares.expiresAt, now)); +} + +export type ShareRepository = ReturnType; + +export function createShareRepository(db: PluginDatabase, tables: Tables) { + const client = () => db.client(); + const { shares, participants } = tables; + + async function findById(id: string): Promise { + const rows = await ( + await client() + ) + .select() + .from(shares) + .where(eq(shares.id, id)) + .limit(1); + return rows[0] ?? null; + } + + return { + findById, + + async create(input: ShareCreateInput): Promise { + await (await client()).insert(shares).values({ + id: input.id, + hostId: input.hostId, + ownerUserId: input.ownerUserId, + protocol: input.protocol, + sessionId: input.sessionId, + tabInstanceId: input.tabInstanceId ?? null, + shareType: input.shareType, + targetUserId: input.targetUserId ?? null, + linkToken: input.linkToken ?? null, + permissionLevel: input.permissionLevel, + expiresAt: input.expiresAt, + }); + await db.persist(); + return (await findById(input.id))!; + }, + + async findActiveById( + id: string, + now = nowIso(), + ): Promise { + const rows = await ( + await client() + ) + .select() + .from(shares) + .where(and(eq(shares.id, id), activeShare(shares, now))) + .limit(1); + return rows[0] ?? null; + }, + + async findByLinkToken( + linkToken: string, + now = nowIso(), + ): Promise { + const rows = await ( + await client() + ) + .select() + .from(shares) + .where(and(eq(shares.linkToken, linkToken), activeShare(shares, now))) + .limit(1); + return rows[0] ?? null; + }, + + async findActiveSharesForHost( + hostId: number, + ownerUserId: string, + now = nowIso(), + ): Promise { + return (await client()) + .select() + .from(shares) + .where( + and( + eq(shares.hostId, hostId), + eq(shares.ownerUserId, ownerUserId), + activeShare(shares, now), + ), + ); + }, + + /** Active user shares aimed at this user. */ + async findSharesTargetingUser( + userId: string, + now = nowIso(), + ): Promise { + return (await client()) + .select() + .from(shares) + .where( + and( + eq(shares.shareType, "user"), + eq(shares.targetUserId, userId), + activeShare(shares, now), + ), + ); + }, + + /** Revokes a share the given user owns. False when it is not theirs. */ + async revoke(shareId: string, ownerUserId: string): Promise { + const share = await findById(shareId); + if (!share || share.ownerUserId !== ownerUserId) return false; + await ( + await client() + ) + .update(shares) + .set({ revokedAt: nowIso() }) + .where(eq(shares.id, shareId)); + await db.persist(); + return true; + }, + + async revokeAny(shareId: string): Promise { + if (!(await findById(shareId))) return false; + await ( + await client() + ) + .update(shares) + .set({ revokedAt: nowIso() }) + .where(eq(shares.id, shareId)); + await db.persist(); + return true; + }, + + async touchUsage(shareId: string): Promise { + const current = await findById(shareId); + if (!current) return; + await ( + await client() + ) + .update(shares) + .set({ + lastJoinedAt: nowIso(), + joinCount: (current.joinCount ?? 0) + 1, + }) + .where(eq(shares.id, shareId)); + await db.persist(); + }, + + async recordParticipantJoin( + shareId: string, + userId: string | null, + guestLabel: string | null, + ): Promise { + await ( + await client() + ) + .insert(participants) + .values({ shareId, userId, guestLabel }); + await db.persist(); + }, + }; +} + +export type RoomRepository = ReturnType; + +export function createRoomRepository( + db: PluginDatabase, + tables: Tables, + refs: () => Promise, +) { + const client = () => db.client(); + const { rooms, members } = tables; + + async function findById(id: string): Promise { + const rows = await ( + await client() + ) + .select() + .from(rooms) + .where(eq(rooms.id, id)) + .limit(1); + return rows[0] ?? null; + } + + async function findMember( + roomId: string, + userId: string, + ): Promise { + const rows = await ( + await client() + ) + .select() + .from(members) + .where(and(eq(members.roomId, roomId), eq(members.userId, userId))) + .limit(1); + return rows[0] ?? null; + } + + async function updateStage(roomId: string, stage: RoomStage): Promise { + await (await client()).update(rooms).set(stage).where(eq(rooms.id, roomId)); + await db.persist(); + } + + return { + findById, + findMember, + updateStage, + + async createRoom(input: { + id: string; + name: string; + ownerUserId: string; + persistent: boolean; + }): Promise { + await (await client()).insert(rooms).values(input); + await db.persist(); + return (await findById(input.id))!; + }, + + /** The live room whose stage points at this share, if any. */ + async findByStageShareId(shareId: string): Promise { + const rows = await ( + await client() + ) + .select() + .from(rooms) + .where(and(eq(rooms.stageShareId, shareId), isNull(rooms.endedAt))) + .limit(1); + return rows[0] ?? null; + }, + + async findByGuestToken(token: string): Promise { + const rows = await ( + await client() + ) + .select() + .from(rooms) + .where(and(eq(rooms.guestLinkToken, token), isNull(rooms.endedAt))) + .limit(1); + return rows[0] ?? null; + }, + + async setGuestToken(roomId: string, token: string | null): Promise { + await ( + await client() + ) + .update(rooms) + .set({ guestLinkToken: token }) + .where(eq(rooms.id, roomId)); + await db.persist(); + }, + + async listForUser(userId: string): Promise { + const rows = await ( + await client() + ) + .select({ room: rooms }) + .from(members) + .innerJoin(rooms, eq(members.roomId, rooms.id)) + .where(and(eq(members.userId, userId), isNull(rooms.endedAt))) + .orderBy(desc(rooms.createdAt)); + return rows.map((row: { room: RoomRecord }) => row.room); + }, + + async addMember(input: { + roomId: string; + userId: string; + roomRole: "host" | "member"; + addedBy: string | null; + }): Promise { + if (await findMember(input.roomId, input.userId)) return false; + await (await client()).insert(members).values(input); + await db.persist(); + return true; + }, + + async removeMember(roomId: string, userId: string): Promise { + await ( + await client() + ) + .delete(members) + .where(and(eq(members.roomId, roomId), eq(members.userId, userId))); + await db.persist(); + }, + + async listMembers(roomId: string): Promise { + const rows: RoomMemberRecord[] = await ( + await client() + ) + .select() + .from(members) + .where(eq(members.roomId, roomId)); + const names = await usernames(rows.map((row) => row.userId)); + return rows + .filter((row) => names.has(row.userId)) + .map((row) => ({ + userId: row.userId, + username: names.get(row.userId)!, + roomRole: row.roomRole, + createdAt: row.createdAt, + })) + .sort((a, b) => a.username.localeCompare(b.username)); + }, + + /** + * Sets the stage only if it still points at `expectedShareId`, so two + * members taking the stage at once cannot both win. + */ + async replaceStage( + roomId: string, + expectedShareId: string | null, + stage: RoomStage, + ): Promise { + await ( + await client() + ) + .update(rooms) + .set(stage) + .where( + and( + eq(rooms.id, roomId), + expectedShareId + ? eq(rooms.stageShareId, expectedShareId) + : isNull(rooms.stageShareId), + ), + ); + const after = await findById(roomId); + const changed = !!after && after.stageShareId === stage.stageShareId; + if (changed) await db.persist(); + return changed; + }, + + async clearStage(roomId: string): Promise { + await updateStage(roomId, { + presenterUserId: null, + stageProtocol: null, + stageHostId: null, + stageShareId: null, + }); + }, + + async endRoom(roomId: string): Promise { + await ( + await client() + ) + .update(rooms) + .set({ + endedAt: nowIso(), + presenterUserId: null, + stageProtocol: null, + stageHostId: null, + stageShareId: null, + }) + .where(eq(rooms.id, roomId)); + await db.persist(); + }, + + async deleteRoom(roomId: string): Promise { + const drizzle = await client(); + // Not left to the foreign key: SQLite only cascades with foreign_keys on. + await drizzle.delete(members).where(eq(members.roomId, roomId)); + await drizzle.delete(rooms).where(eq(rooms.id, roomId)); + await db.persist(); + }, + }; + + async function usernames(ids: string[]): Promise> { + if (ids.length === 0) return new Map(); + const { users } = await refs(); + const rows: Array<{ id: string; username: string }> = await ( + await client() + ) + .select({ id: users.id, username: users.username }) + .from(users) + .where(inArray(users.id, ids)); + return new Map(rows.map((row) => [row.id, row.username])); + } +} + +export type Directory = ReturnType; + +/** Users and roles, read through ctx.db.refs, for names and invites. */ +export function createDirectory( + db: PluginDatabase, + refs: () => Promise, +) { + const client = () => db.client(); + + return { + async username(userId: string): Promise { + const { users } = await refs(); + const rows = await ( + await client() + ) + .select({ username: users.username }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + return rows[0]?.username ?? userId; + }, + + async userExists(userId: string): Promise { + const { users } = await refs(); + const rows = await ( + await client() + ) + .select({ id: users.id }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + return rows.length > 0; + }, + + async isAdmin(userId: string): Promise { + const { users } = await refs(); + const rows = await ( + await client() + ) + .select({ isAdmin: users.isAdmin }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + return !!rows[0]?.isAdmin; + }, + + async hostExists(hostId: number): Promise { + const { hosts } = await refs(); + const rows = await ( + await client() + ) + .select({ id: hosts.id }) + .from(hosts) + .where(eq(hosts.id, hostId)) + .limit(1); + return rows.length > 0; + }, + + async roleExists(roleId: number): Promise { + const { roles } = await refs(); + const rows = await ( + await client() + ) + .select({ id: roles.id }) + .from(roles) + .where(eq(roles.id, roleId)) + .limit(1); + return rows.length > 0; + }, + + async roleUserIds(roleId: number): Promise { + const { userRoles } = await refs(); + const rows: Array<{ userId: string }> = await ( + await client() + ) + .select({ userId: userRoles.userId }) + .from(userRoles) + .where(eq(userRoles.roleId, roleId)); + return rows.map((row) => row.userId); + }, + + async listUsers(): Promise> { + const { users } = await refs(); + return (await client()) + .select({ id: users.id, username: users.username }) + .from(users) + .orderBy(users.username); + }, + + async listRoles(): Promise< + Array<{ id: number; name: string; displayName: string | null }> + > { + const { roles } = await refs(); + return (await client()) + .select({ + id: roles.id, + name: roles.name, + displayName: roles.displayName, + }) + .from(roles) + .orderBy(roles.name); + }, + }; +} diff --git a/src/backend/hosts/collab/room-hub.ts b/plugins/session-sharing/src/backend/room-hub.ts similarity index 76% rename from src/backend/hosts/collab/room-hub.ts rename to plugins/session-sharing/src/backend/room-hub.ts index c75d74d61..76f339e00 100644 --- a/src/backend/hosts/collab/room-hub.ts +++ b/plugins/session-sharing/src/backend/room-hub.ts @@ -1,5 +1,5 @@ import type { WebSocket } from "ws"; -import { collabRuntimeStore } from "./runtime-store.js"; +import type { CollabRuntimeStore } from "./runtime-store.js"; const PRESENCE_HEARTBEAT_MS = 15_000; @@ -10,14 +10,16 @@ export interface CollabRoomClient { } /** - * Local WebSocket fan-out plus optional Redis pub/sub for multi-instance room - * events and presence. The terminal WS server feeds subscribe/unsubscribe. + * Local WebSocket fan-out plus the runtime store's pub/sub for room events and + * presence across instances. The terminal socket feeds subscribe and + * unsubscribe through the sessions.sharing service. */ -class CollabRoomHub { +export class CollabRoomHub { private rooms = new Map>(); + private readonly heartbeat: NodeJS.Timeout; - constructor() { - collabRuntimeStore.onEvent((roomId, message) => { + constructor(private readonly store: CollabRuntimeStore) { + store.onEvent((roomId, message) => { if ( "type" in message && typeof message.type === "string" && @@ -27,9 +29,10 @@ class CollabRoomHub { } this.broadcastLocal(roomId, message); }); - setInterval(() => { + this.heartbeat = setInterval(() => { for (const roomId of this.rooms.keys()) void this.refreshPresence(roomId); - }, PRESENCE_HEARTBEAT_MS).unref(); + }, PRESENCE_HEARTBEAT_MS); + this.heartbeat.unref(); } subscribe(roomId: string, client: CollabRoomClient): void { @@ -63,16 +66,18 @@ class CollabRoomHub { broadcast(roomId: string, message: object): void { this.broadcastLocal(roomId, message); - void collabRuntimeStore.publish(roomId, message); + void this.store.publish(roomId, message); } async onlineUsers( roomId: string, ): Promise> { - return collabRuntimeStore.onlineUsers( - roomId, - this.localOnlineUsers(roomId), - ); + return this.store.onlineUsers(roomId, this.localOnlineUsers(roomId)); + } + + dispose(): void { + clearInterval(this.heartbeat); + this.rooms.clear(); } private broadcastLocal(roomId: string, message: object): void { @@ -103,7 +108,7 @@ class CollabRoomHub { private async refreshPresence(roomId: string): Promise { const localUsers = this.localOnlineUsers(roomId); - await collabRuntimeStore.updatePresence(roomId, localUsers); + await this.store.updatePresence(roomId, localUsers); this.broadcast(roomId, { type: "collab_online", roomId, @@ -111,5 +116,3 @@ class CollabRoomHub { }); } } - -export const collabRoomHub = new CollabRoomHub(); diff --git a/plugins/session-sharing/src/backend/room-routes.ts b/plugins/session-sharing/src/backend/room-routes.ts new file mode 100644 index 000000000..2d33e64d6 --- /dev/null +++ b/plugins/session-sharing/src/backend/room-routes.ts @@ -0,0 +1,1368 @@ +import crypto from "node:crypto"; +import type { Request, Response, Router } from "express"; +import { + PROTOCOLS, + TERMINAL_WS_PATH, + actorOf, + clientIp, + isNonEmptyString, + type SharingDeps, +} from "./deps.js"; +import type { LiveProtocol, RoomRecord } from "./repositories.js"; +import type { CollabControlRequest } from "./runtime-store.js"; + +/* + * Known limits, shared with session sharing: + * - Redis synchronizes collaboration events and ephemeral state across + * instances, but live session transports still need WebSocket affinity. + * - Remote desktop stages stay read-only because guacamole-lite cannot revoke + * a writable viewer without disconnecting the whole shared session. + */ + +const STAGE_SHARE_EXPIRY_HOURS = 12; +const MAX_INVITE_TARGETS = 200; +const CONTROL_REQUEST_COOLDOWN_MS = 5000; + +function publicRoom(room: RoomRecord) { + const { guestLinkToken: secret, ...safeRoom } = room; + return { ...safeRoom, guestLinkEnabled: Boolean(secret) }; +} + +function stagePayload(room: RoomRecord) { + return { + presenterUserId: room.presenterUserId, + protocol: room.stageProtocol, + hostId: room.stageHostId, + shareId: room.stageShareId, + }; +} + +/** The pieces of room behaviour the routes and the runtime events share. */ +export function createRoomActions(deps: SharingDeps) { + const { shares, rooms, live, hub, store } = deps; + + async function requireRoomMember( + roomId: string, + userId: string, + ): Promise<{ room: RoomRecord; isHost: boolean } | null> { + const room = await rooms.findById(roomId); + if (!room || room.endedAt) return null; + const member = await rooms.findMember(roomId, userId); + if (!member) return null; + return { room, isHost: member.roomRole === "host" }; + } + + async function revokeStageShare(room: RoomRecord): Promise { + if (!room.stageShareId) return; + const share = await shares.findActiveById(room.stageShareId); + if (!share) return; + if (!(await shares.revokeAny(room.stageShareId))) { + throw new Error("Failed to revoke the active stage share"); + } + if (share.protocol === "ssh") { + await live.disconnectParticipants( + "ssh", + share.sessionId, + share.id, + { reason: "The collaboration stage ended" }, + share.ownerUserId, + ); + void store.publish(room.id, { + type: "collab_internal_stage_revoked", + sessionId: share.sessionId, + shareId: share.id, + ownerUserId: share.ownerUserId, + }); + } + } + + /** Sets the controller everywhere it lives: the store, the live gate, the hub. */ + async function applyStageControl( + room: RoomRecord, + controllerUserId: string | null, + ): Promise { + await store.setController(room.id, controllerUserId); + if (room.stageShareId && room.stageProtocol === "ssh") { + const share = await shares + .findActiveById(room.stageShareId) + .catch(() => null); + if (share) { + await live.setRoomShareControl( + "ssh", + share.sessionId, + share.id, + controllerUserId, + share.ownerUserId, + ); + } + } + hub.broadcast(room.id, { + type: "collab_control_changed", + roomId: room.id, + controllerUserId, + }); + } + + async function clearStaleStage(roomId: string): Promise { + await store.setController(roomId, null); + await rooms.clearStage(roomId); + hub.broadcast(roomId, { + type: "collab_stage_changed", + roomId, + stage: null, + }); + } + + /** The stage's share, when it is still active and its session still live. */ + async function liveStageShare(room: RoomRecord) { + if (!room.stageShareId || !room.stageProtocol) return null; + const share = await shares.findActiveById(room.stageShareId); + if (!share) return null; + const alive = await live.isLive( + room.stageProtocol, + share.sessionId, + share.ownerUserId, + ); + return alive ? share : null; + } + + /** Room events from other instances that touch live sessions here. */ + function onRuntimeEvent(roomId: string, message: object): void { + const event = message as Record; + if (event.type === "collab_control_changed") { + const controllerUserId = + typeof event.controllerUserId === "string" + ? event.controllerUserId + : null; + void rooms + .findById(roomId) + .then(async (room) => { + if (!room?.stageShareId || room.stageProtocol !== "ssh") return; + const share = await shares.findActiveById(room.stageShareId); + if (share) { + await live.setRoomShareControl( + "ssh", + share.sessionId, + share.id, + controllerUserId, + share.ownerUserId, + ); + } + }) + .catch(() => {}); + } + if ( + event.type === "collab_internal_stage_revoked" && + typeof event.sessionId === "string" && + typeof event.shareId === "string" && + typeof event.ownerUserId === "string" + ) { + void live.disconnectParticipants( + "ssh", + event.sessionId, + event.shareId, + { reason: "The collaboration stage ended" }, + event.ownerUserId, + ); + } + } + + return { + requireRoomMember, + revokeStageShare, + applyStageControl, + clearStaleStage, + liveStageShare, + onRuntimeEvent, + }; +} + +export type RoomActions = ReturnType; + +/** + * Collaboration rooms. `/guest/:token` is public (an anonymous room guest + * link); everything else needs the plugin's `use` permission and room + * membership. + */ +export function registerRoomRoutes( + router: Router, + deps: SharingDeps, + actions: RoomActions, +): void { + const { ctx, shares, rooms, directory, live, hub, store, guestLimiter } = + deps; + const { + requireRoomMember, + revokeStageShare, + applyStageControl, + clearStaleStage, + liveStageShare, + } = actions; + const requireUse = ctx.rbac.require("use") as never; + + const audit = ( + action: string, + room: { id: string; name: string }, + details?: Record, + ) => + ctx.audit.record({ + action, + resourceType: "collab_room", + resourceId: room.id, + resourceName: room.name, + details: details ? JSON.stringify(details) : undefined, + success: true, + }); + + const fail = (res: Response, operation: string, message: string) => { + return (error: unknown) => { + ctx.log.error( + `${operation}: ${message}`, + error instanceof Error ? error : undefined, + ); + res.status(500).json({ error: message }); + }; + }; + + /** + * @openapi + * /plugin-api/session-sharing/rooms: + * post: + * summary: Create a collaboration room + * tags: + * - Collab + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [name] + * properties: + * name: + * type: string + * persistent: + * type: boolean + * responses: + * 200: + * description: The new room + * 400: + * description: Missing or too long name + */ + router.post("/rooms", requireUse, async (req: Request, res: Response) => { + const userId = actorOf(ctx); + const { name, persistent } = req.body ?? {}; + if (!isNonEmptyString(name) || name.trim().length > 120) { + return res.status(400).json({ error: "Room name is required" }); + } + try { + const room = await rooms.createRoom({ + id: crypto.randomUUID(), + name: name.trim(), + ownerUserId: userId, + persistent: persistent === true, + }); + await rooms.addMember({ + roomId: room.id, + userId, + roomRole: "host", + addedBy: userId, + }); + await audit("collab_room_create", room); + res.json({ room: publicRoom(room) }); + } catch (error) { + fail(res, "collab_room_create_error", "Failed to create room")(error); + } + }); + + /** + * @openapi + * /plugin-api/session-sharing/rooms: + * get: + * summary: List rooms the caller belongs to + * tags: + * - Collab + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Rooms, without their guest tokens + */ + router.get("/rooms", requireUse, async (_req: Request, res: Response) => { + try { + const list = await rooms.listForUser(actorOf(ctx)); + res.json({ rooms: list.map(publicRoom) }); + } catch (error) { + fail(res, "collab_room_list_error", "Failed to list rooms")(error); + } + }); + + /** + * @openapi + * /plugin-api/session-sharing/rooms/{id}: + * get: + * summary: Get a room with members, online users and stage state + * tags: + * - Collab + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The room + * 404: + * description: Not a member, or no such room + */ + router.get("/rooms/:id", requireUse, async (req: Request, res: Response) => { + const userId = actorOf(ctx); + const roomId = String(req.params.id); + try { + let access = await requireRoomMember(roomId, userId); + if (!access) return res.status(404).json({ error: "Room not found" }); + + // The presenter's own client drives the stage locally and never asks + // for it, so this is where a presenter whose session died is noticed. + if (access.room.stageShareId && !(await liveStageShare(access.room))) { + await clearStaleStage(roomId); + access = await requireRoomMember(roomId, userId); + if (!access) return res.status(404).json({ error: "Room not found" }); + } + + const members = await rooms.listMembers(roomId); + const mayReview = access.isHost || access.room.presenterUserId === userId; + const controlRequests = await store.listRequests(roomId); + res.json({ + room: publicRoom(access.room), + me: userId, + isHost: access.isHost, + members, + online: await hub.onlineUsers(roomId), + stage: stagePayload(access.room), + controllerUserId: await store.getController(roomId), + controlRequests: mayReview + ? controlRequests + : controlRequests.filter((request) => request.userId === userId), + // Room events ride on the terminal socket. + eventsWsPath: TERMINAL_WS_PATH, + }); + } catch (error) { + fail(res, "collab_room_get_error", "Failed to get room")(error); + } + }); + + /** + * @openapi + * /plugin-api/session-sharing/rooms/{id}/members: + * post: + * summary: Invite users to a room (host only) + * description: Roles expand to their current members, a snapshot. + * tags: + * - Collab + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * userIds: + * type: array + * items: + * type: string + * roleIds: + * type: array + * items: + * type: integer + * responses: + * 200: + * description: Invited + * 403: + * description: Not the host + * 404: + * description: Room, user or role not found + */ + router.post( + "/rooms/:id/members", + requireUse, + async (req: Request, res: Response) => { + const userId = actorOf(ctx); + const roomId = String(req.params.id); + const { userIds = [], roleIds = [] } = req.body ?? {}; + + if ( + !Array.isArray(userIds) || + userIds.some((id) => !isNonEmptyString(id)) || + !Array.isArray(roleIds) || + roleIds.some((id) => !Number.isInteger(id)) || + (userIds.length === 0 && roleIds.length === 0) + ) { + return res.status(400).json({ + error: "userIds (user ids) or roleIds (integers) are required", + }); + } + if (userIds.length + roleIds.length > MAX_INVITE_TARGETS) { + return res.status(400).json({ error: "Too many invite targets" }); + } + + try { + const access = await requireRoomMember(roomId, userId); + if (!access) return res.status(404).json({ error: "Room not found" }); + if (!access.isHost) { + return res.status(403).json({ error: "Only the host can invite" }); + } + + for (const targetId of userIds as string[]) { + if (!(await directory.userExists(targetId))) { + return res.status(404).json({ error: "User not found", targetId }); + } + } + const expanded = new Set(userIds as string[]); + for (const roleId of roleIds as number[]) { + if (!(await directory.roleExists(roleId))) { + return res.status(404).json({ error: "Role not found", roleId }); + } + for (const memberId of await directory.roleUserIds(roleId)) { + expanded.add(memberId); + } + } + for (const targetId of expanded) { + await rooms.addMember({ + roomId, + userId: targetId, + roomRole: "member", + addedBy: userId, + }); + } + + await audit("collab_room_invite", access.room, { + memberCount: expanded.size, + }); + hub.broadcast(roomId, { type: "collab_members_changed", roomId }); + res.json({ success: true }); + } catch (error) { + fail( + res, + "collab_room_invite_error", + "Failed to invite members", + )(error); + } + }, + ); + + /** + * @openapi + * /plugin-api/session-sharing/rooms/{id}/members/{userId}: + * delete: + * summary: Remove a member (host), or leave the room (self) + * tags: + * - Collab + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * - in: path + * name: userId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Removed + * 400: + * description: The owner cannot be removed + * 403: + * description: Only the host can remove others + */ + router.delete( + "/rooms/:id/members/:userId", + requireUse, + async (req: Request, res: Response) => { + const userId = actorOf(ctx); + const roomId = String(req.params.id); + const targetId = String(req.params.userId); + try { + const access = await requireRoomMember(roomId, userId); + if (!access) return res.status(404).json({ error: "Room not found" }); + if (targetId !== userId && !access.isHost) { + return res + .status(403) + .json({ error: "Only the host can remove members" }); + } + if (targetId === access.room.ownerUserId) { + return res.status(400).json({ error: "The owner cannot be removed" }); + } + + await rooms.removeMember(roomId, targetId); + await store.removeRequest(roomId, targetId); + + if (access.room.stageShareId && access.room.stageProtocol === "ssh") { + const share = await shares.findActiveById(access.room.stageShareId); + if (share) { + await live.disconnectParticipants( + "ssh", + share.sessionId, + share.id, + { + userId: targetId, + reason: "You were removed from the collaboration room", + }, + share.ownerUserId, + ); + } + } + + if ((await store.getController(roomId)) === targetId) { + await applyStageControl(access.room, null); + } + + if (access.room.presenterUserId === targetId) { + await revokeStageShare(access.room); + await rooms.clearStage(roomId); + hub.broadcast(roomId, { + type: "collab_stage_changed", + roomId, + stage: null, + }); + } + + await audit( + targetId === userId + ? "collab_room_leave" + : "collab_room_remove_member", + access.room, + { targetUserId: targetId }, + ); + hub.broadcast(roomId, { type: "collab_members_changed", roomId }); + res.json({ success: true }); + } catch (error) { + fail( + res, + "collab_room_remove_member_error", + "Failed to remove member", + )(error); + } + }, + ); + + /** + * @openapi + * /plugin-api/session-sharing/rooms/{id}/present: + * post: + * summary: Take the stage with one of your live sessions + * description: Any member may take over the stage; the previous stage share is revoked. The caller must own the live session and sharing must be enabled for the host. + * tags: + * - Collab + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [protocol, sessionId, hostId] + * properties: + * protocol: + * type: string + * enum: [ssh, rdp, vnc, telnet] + * sessionId: + * type: string + * hostId: + * type: integer + * responses: + * 200: + * description: The new stage + * 403: + * description: Sharing disabled, or not the session's owner + * 409: + * description: The stage changed meanwhile + */ + router.post( + "/rooms/:id/present", + requireUse, + async (req: Request, res: Response) => { + const userId = actorOf(ctx); + const roomId = String(req.params.id); + const { protocol, sessionId, hostId } = req.body ?? {}; + + if (!PROTOCOLS.includes(protocol)) { + return res.status(400).json({ error: "Invalid protocol" }); + } + if (!isNonEmptyString(sessionId) || !Number.isInteger(Number(hostId))) { + return res + .status(400) + .json({ error: "sessionId and hostId are required" }); + } + + try { + const access = await requireRoomMember(roomId, userId); + if (!access) return res.status(404).json({ error: "Room not found" }); + + const numericHostId = Number(hostId); + if (!(await deps.isSharingEnabledForHost(numericHostId))) { + return res + .status(403) + .json({ error: "Session sharing is disabled for this host" }); + } + if ( + !(await live.isOwnedBy( + protocol as LiveProtocol, + String(sessionId), + userId, + )) + ) { + return res + .status(403) + .json({ error: "You do not own this live session" }); + } + + const share = await shares.create({ + id: crypto.randomUUID(), + hostId: numericHostId, + ownerUserId: userId, + protocol, + sessionId: String(sessionId), + shareType: "room", + permissionLevel: "read-only", + expiresAt: new Date( + Date.now() + STAGE_SHARE_EXPIRY_HOURS * 60 * 60 * 1000, + ).toISOString(), + }); + + await revokeStageShare(access.room); + const replaced = await rooms.replaceStage( + roomId, + access.room.stageShareId, + { + presenterUserId: userId, + stageProtocol: protocol, + stageHostId: numericHostId, + stageShareId: share.id, + }, + ); + if (!replaced) { + await shares.revokeAny(share.id); + return res + .status(409) + .json({ error: "The stage changed; try again" }); + } + await store.setController(roomId, null); + await store.clearRequests(roomId); + + const stage = { + presenterUserId: userId, + protocol, + hostId: numericHostId, + shareId: share.id, + }; + hub.broadcast(roomId, { type: "collab_stage_changed", roomId, stage }); + await audit("collab_room_present", access.room, { + protocol, + hostId: numericHostId, + }); + res.json({ stage }); + } catch (error) { + fail( + res, + "collab_room_present_error", + "Failed to start presenting", + )(error); + } + }, + ); + + /** + * @openapi + * /plugin-api/session-sharing/rooms/{id}/stop: + * post: + * summary: Stop presenting (presenter or host) + * tags: + * - Collab + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Stage cleared + * 403: + * description: Not the presenter or host + */ + router.post( + "/rooms/:id/stop", + requireUse, + async (req: Request, res: Response) => { + const userId = actorOf(ctx); + const roomId = String(req.params.id); + try { + const access = await requireRoomMember(roomId, userId); + if (!access) return res.status(404).json({ error: "Room not found" }); + if (access.room.presenterUserId !== userId && !access.isHost) { + return res + .status(403) + .json({ error: "Only the presenter or host can stop the stage" }); + } + + await revokeStageShare(access.room); + await store.setController(roomId, null); + await store.clearRequests(roomId); + await rooms.clearStage(roomId); + hub.broadcast(roomId, { + type: "collab_stage_changed", + roomId, + stage: null, + }); + await audit("collab_room_stop_presenting", access.room); + res.json({ success: true }); + } catch (error) { + fail(res, "collab_room_stop_error", "Failed to stop presenting")(error); + } + }, + ); + + /** + * @openapi + * /plugin-api/session-sharing/rooms/{id}/stage: + * get: + * summary: Get connect info for the current stage (members only) + * description: SSH stages are joined over the terminal socket by shareId; remote desktop stages get a freshly minted read-only viewer token. + * tags: + * - Collab + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The stage, or null + */ + router.get( + "/rooms/:id/stage", + requireUse, + async (req: Request, res: Response) => { + const userId = actorOf(ctx); + const roomId = String(req.params.id); + try { + const access = await requireRoomMember(roomId, userId); + if (!access) return res.status(404).json({ error: "Room not found" }); + const { room } = access; + if (!room.stageShareId || !room.stageProtocol) { + return res.json({ stage: null }); + } + + const share = await liveStageShare(room); + if (!share) { + // The presenter is gone (expired share or dead session). + await clearStaleStage(roomId); + return res.json({ stage: null }); + } + + const stage: Record = { + ...stagePayload(room), + sessionId: share.sessionId, + controllerUserId: await store.getController(roomId), + }; + if (room.stageProtocol !== "ssh") { + stage.connectParams = { + token: await live.createViewerToken( + room.stageProtocol, + share.sessionId, + true, + share.ownerUserId, + ), + }; + } + res.json({ stage }); + } catch (error) { + fail(res, "collab_room_stage_error", "Failed to resolve stage")(error); + } + }, + ); + + /** + * @openapi + * /plugin-api/session-sharing/rooms/{id}/control: + * post: + * summary: Grant or revoke stage control (presenter or host) + * description: Grants a member write access to the current SSH stage, or revokes it with a null userId. The controller may also release control themselves. + * tags: + * - Collab + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * userId: + * type: string + * nullable: true + * responses: + * 200: + * description: The new controller + * 400: + * description: Nothing presented, or a remote desktop stage + * 403: + * description: Not the presenter or host + */ + router.post( + "/rooms/:id/control", + requireUse, + async (req: Request, res: Response) => { + const userId = actorOf(ctx); + const roomId = String(req.params.id); + const { userId: targetId } = req.body ?? {}; + if (targetId !== null && !isNonEmptyString(targetId)) { + return res + .status(400) + .json({ error: "userId must be a user id or null" }); + } + + try { + const access = await requireRoomMember(roomId, userId); + if (!access) return res.status(404).json({ error: "Room not found" }); + if (!access.room.stageShareId) { + return res.status(400).json({ error: "Nothing is being presented" }); + } + if (access.room.stageProtocol !== "ssh") { + return res + .status(400) + .json({ error: "Remote desktop stages are read-only" }); + } + + const releasingOwn = + targetId === null && (await store.getController(roomId)) === userId; + const mayGrant = + access.isHost || access.room.presenterUserId === userId; + if (!mayGrant && !releasingOwn) { + return res.status(403).json({ + error: "Only the presenter or host can change stage control", + }); + } + if (targetId && !(await rooms.findMember(roomId, targetId))) { + return res.status(404).json({ error: "Member not found" }); + } + + await applyStageControl(access.room, targetId); + if (targetId) { + await store.removeRequest(roomId, targetId); + hub.broadcast(roomId, { + type: "collab_control_requests_changed", + roomId, + }); + } + await audit( + targetId ? "collab_control_grant" : "collab_control_revoke", + access.room, + { targetUserId: targetId }, + ); + res.json({ controllerUserId: targetId }); + } catch (error) { + fail( + res, + "collab_control_error", + "Failed to change stage control", + )(error); + } + }, + ); + + /** + * @openapi + * /plugin-api/session-sharing/rooms/{id}/control/request: + * post: + * summary: Ask the presenter for stage control (hand raise) + * tags: + * - Collab + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The request + * 429: + * description: Asked too recently + */ + router.post( + "/rooms/:id/control/request", + requireUse, + async (req: Request, res: Response) => { + const userId = actorOf(ctx); + const roomId = String(req.params.id); + try { + const access = await requireRoomMember(roomId, userId); + if (!access) return res.status(404).json({ error: "Room not found" }); + if (!access.room.stageShareId) { + return res.status(400).json({ error: "Nothing is being presented" }); + } + if (access.room.stageProtocol !== "ssh") { + return res + .status(400) + .json({ error: "Remote desktop stages are read-only" }); + } + const existing = (await store.listRequests(roomId)).find( + (request) => request.userId === userId, + ); + if ( + existing && + Date.now() - Date.parse(existing.requestedAt) < + CONTROL_REQUEST_COOLDOWN_MS + ) { + return res + .status(429) + .json({ error: "Control was already requested" }); + } + const request: CollabControlRequest = { + userId, + username: await directory.username(userId), + requestedAt: new Date().toISOString(), + }; + await store.upsertRequest(roomId, request); + hub.broadcast(roomId, { + type: "collab_control_requested", + roomId, + ...request, + }); + await audit("collab_control_request", access.room); + res.json({ request }); + } catch (error) { + fail( + res, + "collab_control_request_error", + "Failed to request control", + )(error); + } + }, + ); + + /** + * @openapi + * /plugin-api/session-sharing/rooms/{id}/control/requests: + * get: + * summary: List pending control requests (presenter or host) + * tags: + * - Collab + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Pending requests + * 403: + * description: Not the presenter or host + */ + router.get( + "/rooms/:id/control/requests", + requireUse, + async (req: Request, res: Response) => { + const userId = actorOf(ctx); + const roomId = String(req.params.id); + try { + const access = await requireRoomMember(roomId, userId); + if (!access) return res.status(404).json({ error: "Room not found" }); + if (!access.isHost && access.room.presenterUserId !== userId) { + return res.status(403).json({ + error: "Only the presenter or host can review control requests", + }); + } + res.json({ requests: await store.listRequests(roomId) }); + } catch (error) { + fail( + res, + "collab_control_requests_list_error", + "Failed to list control requests", + )(error); + } + }, + ); + + /** + * @openapi + * /plugin-api/session-sharing/rooms/{id}/control/requests/{userId}: + * delete: + * summary: Dismiss a control request (presenter or host), or cancel your own + * tags: + * - Collab + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * - in: path + * name: userId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Dismissed + * 403: + * description: Not allowed to dismiss it + */ + router.delete( + "/rooms/:id/control/requests/:userId", + requireUse, + async (req: Request, res: Response) => { + const userId = actorOf(ctx); + const roomId = String(req.params.id); + const targetId = String(req.params.userId); + try { + const access = await requireRoomMember(roomId, userId); + if (!access) return res.status(404).json({ error: "Room not found" }); + const mayReview = + access.isHost || access.room.presenterUserId === userId; + if (!mayReview && targetId !== userId) { + return res + .status(403) + .json({ error: "Not allowed to dismiss request" }); + } + await store.removeRequest(roomId, targetId); + hub.broadcast(roomId, { + type: "collab_control_requests_changed", + roomId, + }); + await audit( + targetId === userId + ? "collab_control_request_cancel" + : "collab_control_request_dismiss", + access.room, + { targetUserId: targetId }, + ); + res.json({ success: true }); + } catch (error) { + fail( + res, + "collab_control_request_dismiss_error", + "Failed to dismiss control request", + )(error); + } + }, + ); + + /** + * @openapi + * /plugin-api/session-sharing/rooms/{id}/guest-link: + * post: + * summary: Enable, rotate or disable the room's anonymous guest link (host only) + * tags: + * - Collab + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [enabled] + * properties: + * enabled: + * type: boolean + * responses: + * 200: + * description: The new token, or null when disabled + * 403: + * description: Not the host + */ + router.post( + "/rooms/:id/guest-link", + requireUse, + async (req: Request, res: Response) => { + const userId = actorOf(ctx); + const roomId = String(req.params.id); + const { enabled } = req.body ?? {}; + if (typeof enabled !== "boolean") { + return res.status(400).json({ error: "enabled must be a boolean" }); + } + try { + const access = await requireRoomMember(roomId, userId); + if (!access) return res.status(404).json({ error: "Room not found" }); + if (!access.isHost) { + return res + .status(403) + .json({ error: "Only the host can manage the guest link" }); + } + const token = enabled + ? crypto.randomBytes(24).toString("base64url") + : null; + await rooms.setGuestToken(roomId, token); + + // A rotated or disabled link drops the guests who came in on the old one. + if ( + (!enabled || access.room.guestLinkToken) && + access.room.stageShareId && + access.room.stageProtocol === "ssh" + ) { + const share = await shares.findActiveById(access.room.stageShareId); + if (share) { + await live.disconnectParticipants( + "ssh", + share.sessionId, + share.id, + { + userId: null, + reason: enabled + ? "The guest link was rotated" + : "The guest link was disabled", + }, + share.ownerUserId, + ); + } + } + + await audit( + enabled ? "collab_guest_link_enable" : "collab_guest_link_disable", + access.room, + ); + res.json({ guestLinkToken: token }); + } catch (error) { + fail( + res, + "collab_guest_link_error", + "Failed to update guest link", + )(error); + } + }, + ); + + /** + * @openapi + * /plugin-api/session-sharing/guest/{token}: + * get: + * summary: Resolve a room's current stage for an anonymous guest + * description: Public, rate-limited per IP. Guests poll this to follow presenter switches. Never returns host details; SSH stages are joined over the terminal socket with roomGuestToken, remote desktop stages get a read-only viewer token. + * tags: + * - Collab + * parameters: + * - in: path + * name: token + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Room name and the stage, or a null stage + * 404: + * description: Link not found + * 429: + * description: Too many requests + */ + router.get("/guest/:token", async (req: Request, res: Response) => { + if (guestLimiter.isLimited(clientIp(req))) { + return res.status(429).json({ error: "Too many requests" }); + } + const token = String(req.params.token); + try { + const room = await rooms.findByGuestToken(token); + if (!room) return res.status(404).json({ error: "Link not found" }); + + const response: Record = { + roomName: room.name, + stage: null, + }; + const share = await liveStageShare(room); + if ( + share && + room.stageProtocol && + (await deps.isSharingEnabledForHost(share.hostId)) + ) { + const protocol = room.stageProtocol as LiveProtocol; + response.stage = { + protocol, + shareId: share.id, + ...(protocol === "ssh" + ? { + wsPath: `${TERMINAL_WS_PATH}?roomGuestToken=${encodeURIComponent(token)}`, + } + : { + connectParams: { + token: await live.createViewerToken( + protocol, + share.sessionId, + true, + share.ownerUserId, + ), + }, + }), + }; + } + res.json(response); + } catch (error) { + fail( + res, + "collab_guest_resolve_error", + "Failed to resolve guest link", + )(error); + } + }); + + /** + * @openapi + * /plugin-api/session-sharing/rooms/{id}/end: + * post: + * summary: End the meeting (host only) + * description: Clears the stage. One-off rooms are ended for good; persistent rooms stay listed for reuse. + * tags: + * - Collab + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Ended + * 403: + * description: Not the host + */ + router.post( + "/rooms/:id/end", + requireUse, + async (req: Request, res: Response) => { + const userId = actorOf(ctx); + const roomId = String(req.params.id); + try { + const access = await requireRoomMember(roomId, userId); + if (!access) return res.status(404).json({ error: "Room not found" }); + if (!access.isHost) { + return res + .status(403) + .json({ error: "Only the host can end the room" }); + } + + await revokeStageShare(access.room); + await store.setController(roomId, null); + await store.clearRequests(roomId); + if (access.room.persistent) { + await rooms.setGuestToken(roomId, null); + await rooms.clearStage(roomId); + hub.broadcast(roomId, { + type: "collab_stage_changed", + roomId, + stage: null, + }); + } else { + await rooms.endRoom(roomId); + hub.broadcast(roomId, { type: "collab_room_ended", roomId }); + } + await audit("collab_room_end", access.room); + res.json({ success: true }); + } catch (error) { + fail(res, "collab_room_end_error", "Failed to end room")(error); + } + }, + ); + + /** + * @openapi + * /plugin-api/session-sharing/rooms/{id}: + * delete: + * summary: Permanently delete a room (host only) + * description: Revokes the active stage and removes the room and its membership. This cannot be undone. + * tags: + * - Collab + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Deleted + * 403: + * description: Not the host + */ + router.delete( + "/rooms/:id", + requireUse, + async (req: Request, res: Response) => { + const userId = actorOf(ctx); + const roomId = String(req.params.id); + try { + const access = await requireRoomMember(roomId, userId); + if (!access) return res.status(404).json({ error: "Room not found" }); + if (!access.isHost) { + return res + .status(403) + .json({ error: "Only the host can delete the room" }); + } + + await revokeStageShare(access.room); + await store.setController(roomId, null); + await store.clearRequests(roomId); + hub.broadcast(roomId, { type: "collab_room_ended", roomId }); + await rooms.deleteRoom(roomId); + await audit("collab_room_delete", access.room); + res.json({ success: true }); + } catch (error) { + fail(res, "collab_room_delete_error", "Failed to delete room")(error); + } + }, + ); +} diff --git a/src/backend/hosts/collab/runtime-store.ts b/plugins/session-sharing/src/backend/runtime-store.ts similarity index 92% rename from src/backend/hosts/collab/runtime-store.ts rename to plugins/session-sharing/src/backend/runtime-store.ts index 51b98be53..28162110d 100644 --- a/src/backend/hosts/collab/runtime-store.ts +++ b/plugins/session-sharing/src/backend/runtime-store.ts @@ -1,6 +1,6 @@ -import crypto from "crypto"; +import crypto from "node:crypto"; import { createClient } from "redis"; -import { sshLogger } from "../../utils/logger.js"; +import type { PluginLogger } from "@termix/plugin-sdk/backend"; export interface CollabControlRequest { userId: string; @@ -26,7 +26,14 @@ const PRESENCE_TTL_MS = 45_000; const STATE_TTL_SECONDS = 12 * 60 * 60; const CONNECT_RETRY_MS = 15_000; +/** + * Room state that has to be shared between Termix instances: who controls a + * stage, pending control requests, presence and room events. Redis-backed when + * REDIS_URL is set, with local state for a single instance. + */ export class CollabRuntimeStore { + constructor(private readonly log: PluginLogger) {} + private readonly instanceId = crypto.randomUUID(); private publisher: ReturnType | null = null; private subscriber: ReturnType | null = null; @@ -192,6 +199,8 @@ export class CollabRuntimeStore { } async close(): Promise { + this.eventListeners.clear(); + this.nextConnectAttempt = Number.POSITIVE_INFINITY; await Promise.allSettled([ this.publisher?.isOpen ? this.publisher.quit() : Promise.resolve(), this.subscriber?.isOpen ? this.subscriber.quit() : Promise.resolve(), @@ -246,9 +255,7 @@ export class CollabRuntimeStore { } } }); - sshLogger.info("Collaboration Redis runtime connected", { - operation: "collab_redis_connected", - }); + this.log.info("Collaboration Redis runtime connected"); this.nextConnectAttempt = 0; return true; } catch (error) { @@ -273,14 +280,10 @@ export class CollabRuntimeStore { } private logRedisFailure(operation: string, error: unknown): void { - sshLogger.warn( - "Collaboration Redis runtime unavailable; using local state", - { - operation: `collab_redis_${operation}`, - error: error instanceof Error ? error.message : String(error), - }, + this.log.warn( + `Collaboration Redis runtime unavailable (${operation}), using local state: ${ + error instanceof Error ? error.message : String(error) + }`, ); } } - -export const collabRuntimeStore = new CollabRuntimeStore(); diff --git a/plugins/session-sharing/src/backend/services.ts b/plugins/session-sharing/src/backend/services.ts new file mode 100644 index 000000000..bb3ddbcc8 --- /dev/null +++ b/plugins/session-sharing/src/backend/services.ts @@ -0,0 +1,158 @@ +import type { WebSocket } from "ws"; +import type { SharingDeps } from "./deps.js"; +import type { ShareRecord } from "./repositories.js"; + +/** + * What the terminal consumes. The shapes match + * plugins/ssh-terminal/src/backend/services.ts, which is the consumer's copy. + */ +export interface SharedSessionRef { + id: string; + sessionId: string; + permissionLevel: "read-write" | "read-only"; +} + +/** sessions.sharing v1: signed-in joins. Every call runs as the acting user. */ +export interface SessionSharingV1 { + authorizeJoin: ( + shareId: string, + ) => Promise<{ share: SharedSessionRef; displayName: string } | null>; + recordJoin: (shareId: string) => Promise; + subscribeRoom: (roomId: string, socket: unknown) => Promise; + unsubscribeRoom: (socket: unknown, roomId?: string) => Promise; +} + +/** + * Guest joins through a share link or a room guest link, on ctx.registry as + * SESSION_GUESTS_KEY. A guest has no user, so a service's per-call + * permission check cannot apply: the token is the authority. + */ +export interface SessionGuestsV1 { + resolve: (request: { + shareToken?: string; + roomGuestToken?: string; + clientIp: string; + }) => Promise< + { ok: true; share: SharedSessionRef } | { ok: false; reason: string } + >; + recordJoin: (shareId: string) => Promise; +} + +export const SESSION_GUESTS_KEY = "sessions.sharing.guests"; + +function toRef(share: ShareRecord): SharedSessionRef { + return { + id: share.id, + sessionId: share.sessionId, + permissionLevel: + share.permissionLevel === "read-write" ? "read-write" : "read-only", + }; +} + +export function createSessionSharingService( + deps: SharingDeps, +): SessionSharingV1 { + const { ctx, shares, rooms, directory, hub } = deps; + + return { + async authorizeJoin(shareId) { + const userId = ctx.currentActor(); + if (!userId) return null; + const share = await shares.findActiveById(shareId); + if (!share || share.protocol !== "ssh") return null; + + if (share.shareType === "user") { + // A user share needs its target, who can still reach the host. + if (share.targetUserId !== userId) return null; + const access = await ctx.hosts.checkAccess(share.hostId, "connect"); + if (!access.hasAccess) return null; + } else if (share.shareType === "room") { + // Room membership is the authorization for a room stage: it is + // read-only and never exposes host credentials or config. + const room = await rooms.findByStageShareId(share.id); + if (!room || !(await rooms.findMember(room.id, userId))) return null; + } else { + return null; + } + + return { + share: toRef(share), + displayName: await directory.username(userId), + }; + }, + + async recordJoin(shareId) { + const userId = ctx.currentActor() ?? null; + await shares.touchUsage(shareId); + await shares.recordParticipantJoin(shareId, userId, null); + }, + + async subscribeRoom(roomId, socket) { + const userId = ctx.currentActor(); + if (!userId) return false; + const room = await rooms.findById(roomId); + if (!room || room.endedAt) return false; + if (!(await rooms.findMember(roomId, userId))) return false; + hub.subscribe(roomId, { + ws: socket as WebSocket, + userId, + username: await directory.username(userId), + }); + return true; + }, + + async unsubscribeRoom(socket, roomId) { + hub.unsubscribe(socket as WebSocket, roomId); + }, + }; +} + +export function createSessionGuests(deps: SharingDeps): SessionGuestsV1 { + const { shares, rooms, guestLimiter } = deps; + + /** Sharing still allowed, and an SSH share: the only kind the socket joins. */ + async function joinable( + share: ShareRecord, + ): Promise< + { ok: true; share: SharedSessionRef } | { ok: false; reason: string } + > { + if (share.protocol !== "ssh") { + return { ok: false, reason: "Unsupported share protocol" }; + } + if (!(await deps.isSharingEnabledForHost(share.hostId))) { + return { ok: false, reason: "Session sharing is disabled for this host" }; + } + return { ok: true, share: toRef(share) }; + } + + return { + async resolve({ shareToken, roomGuestToken, clientIp }) { + if (shareToken) { + const share = await shares.findByLinkToken(shareToken); + if (!share || share.shareType !== "link") { + return { ok: false, reason: "Invalid or expired share link" }; + } + return joinable(share); + } + + if (roomGuestToken) { + if (guestLimiter.isLimited(clientIp)) { + return { ok: false, reason: "Too many requests" }; + } + const room = await rooms.findByGuestToken(roomGuestToken); + const share = room?.stageShareId + ? await shares.findActiveById(room.stageShareId) + : null; + if (!share) return { ok: false, reason: "Nothing is being presented" }; + return joinable(share); + } + + return { ok: false, reason: "Authentication required" }; + }, + + async recordJoin(shareId) { + await shares.touchUsage(shareId); + await shares.recordParticipantJoin(shareId, null, "Guest"); + }, + }; +} diff --git a/plugins/session-sharing/src/backend/share-routes.ts b/plugins/session-sharing/src/backend/share-routes.ts new file mode 100644 index 000000000..c154ca496 --- /dev/null +++ b/plugins/session-sharing/src/backend/share-routes.ts @@ -0,0 +1,523 @@ +import crypto from "node:crypto"; +import type { Request, Response, Router } from "express"; +import { + DISPLAY_WS_PATH, + PROTOCOLS, + TERMINAL_WS_PATH, + actorOf, + clientIp, + type SharingDeps, +} from "./deps.js"; +import type { LiveProtocol, PermissionLevel } from "./repositories.js"; + +const DEFAULT_EXPIRY_HOURS = 24; +const MAX_EXPIRY_HOURS = 24 * 30; + +function computeExpiresAt(expiryHours: number | undefined): string { + const hours = Math.min( + Math.max(expiryHours ?? DEFAULT_EXPIRY_HOURS, 1), + MAX_EXPIRY_HOURS, + ); + return new Date(Date.now() + hours * 60 * 60 * 1000).toISOString(); +} + +/** + * The share routes. `/resolve/:linkToken` is public (an anonymous guest link); + * the rest run behind core auth and the plugin's `use` permission, which the + * caller applies to the router first. + */ +export function registerShareRoutes(router: Router, deps: SharingDeps): void { + const { ctx, shares, directory, live, resolveLimiter } = deps; + const requireUse = ctx.rbac.require("use") as never; + + /** + * @openapi + * /plugin-api/session-sharing/resolve/{linkToken}: + * get: + * summary: Resolve a guest share link + * description: Public, unauthenticated endpoint for anonymous share-link guests. Never returns host name, IP, username, or hostId. Rate-limited per IP. + * tags: + * - Session Sharing + * parameters: + * - in: path + * name: linkToken + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Resolved share connection info + * 404: + * description: Link not found, expired, revoked, or sharing disabled + * 429: + * description: Too many requests + * 500: + * description: Server error + */ + router.get("/resolve/:linkToken", async (req: Request, res: Response) => { + try { + if (resolveLimiter.isLimited(clientIp(req))) { + return res.status(429).json({ error: "Too many requests" }); + } + + const linkToken = String(req.params.linkToken); + const share = await shares.findByLinkToken(linkToken); + if (!share || share.shareType !== "link") { + return res.status(404).json({ error: "Link not found or expired" }); + } + if (!(await deps.isSharingEnabledForHost(share.hostId))) { + return res.status(404).json({ error: "Link not found or expired" }); + } + + const protocol = share.protocol as LiveProtocol; + if (!(await live.isLive(protocol, share.sessionId, share.ownerUserId))) { + return res.status(404).json({ error: "Session is no longer active" }); + } + + // Field by field on purpose: anonymous guests never see the host's + // name, address, username or id. + const response: { + protocol: LiveProtocol; + permissionLevel: PermissionLevel; + wsPath: string; + connectParams?: Record; + } = { + protocol, + permissionLevel: share.permissionLevel as PermissionLevel, + // Advisory only: the client builds its own socket URL per deployment. + wsPath: + protocol === "ssh" + ? `${TERMINAL_WS_PATH}?shareToken=${encodeURIComponent(linkToken)}` + : DISPLAY_WS_PATH, + }; + + if (protocol !== "ssh") { + response.connectParams = { + token: await live.createViewerToken( + protocol, + share.sessionId, + share.permissionLevel === "read-only", + share.ownerUserId, + ), + }; + } + + try { + await shares.touchUsage(share.id); + await shares.recordParticipantJoin(share.id, null, "Guest"); + } catch { + // Bookkeeping never fails the resolve. + } + + res.json(response); + } catch (error) { + ctx.log.error( + "Failed to resolve session share link", + error instanceof Error ? error : undefined, + ); + res.status(500).json({ error: "Failed to resolve share link" }); + } + }); + + /** + * @openapi + * /plugin-api/session-sharing/create: + * post: + * summary: Create a session share (link or targeted user) + * description: Mints a share grant for a live terminal/RDP/VNC/Telnet session. Caller must own the live session. + * tags: + * - Session Sharing + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - hostId + * - sessionId + * - protocol + * - shareType + * - permissionLevel + * properties: + * hostId: + * type: integer + * sessionId: + * type: string + * tabInstanceId: + * type: string + * protocol: + * type: string + * enum: [ssh, rdp, vnc, telnet] + * shareType: + * type: string + * enum: [link, user] + * targetUserId: + * type: string + * permissionLevel: + * type: string + * enum: [read-only, read-write] + * expiryHours: + * type: number + * responses: + * 200: + * description: Share created + * 400: + * description: Invalid request + * 403: + * description: Sharing disabled, or caller does not own the session + * 500: + * description: Server error + */ + router.post("/create", requireUse, async (req: Request, res: Response) => { + try { + const userId = actorOf(ctx); + const { + hostId, + sessionId, + tabInstanceId, + protocol, + shareType, + targetUserId, + permissionLevel, + expiryHours, + } = req.body ?? {}; + + if ( + !hostId || + !sessionId || + !protocol || + !shareType || + !permissionLevel + ) { + return res.status(400).json({ error: "Missing required fields" }); + } + if (!PROTOCOLS.includes(protocol)) { + return res.status(400).json({ error: "Invalid protocol" }); + } + if (!["link", "user"].includes(shareType)) { + return res.status(400).json({ error: "Invalid shareType" }); + } + if (!["read-only", "read-write"].includes(permissionLevel)) { + return res.status(400).json({ error: "Invalid permissionLevel" }); + } + if (shareType === "user" && !targetUserId) { + return res + .status(400) + .json({ error: "targetUserId is required for user shares" }); + } + + const numericHostId = Number(hostId); + if (!(await deps.isSharingEnabledForHost(numericHostId))) { + return res + .status(403) + .json({ error: "Session sharing is disabled for this host" }); + } + if (!(await live.isOwnedBy(protocol, String(sessionId), userId))) { + return res + .status(403) + .json({ error: "You do not own this live session" }); + } + + if (shareType === "user") { + const target = String(targetUserId); + const access = (await directory.userExists(target)) + ? await ctx.asUser(target, () => + ctx.hosts.checkAccess(numericHostId, "connect"), + ) + : { hasAccess: false }; + if (!access.hasAccess) { + return res.status(403).json({ + error: "Target user does not have access to this host", + }); + } + } + + const created = await shares.create({ + id: crypto.randomUUID(), + hostId: numericHostId, + ownerUserId: userId, + protocol, + sessionId: String(sessionId), + tabInstanceId: tabInstanceId ?? null, + shareType, + targetUserId: shareType === "user" ? String(targetUserId) : null, + linkToken: + shareType === "link" + ? crypto.randomBytes(24).toString("base64url") + : null, + permissionLevel, + expiresAt: computeExpiresAt(expiryHours), + }); + + res.json({ + shareId: created.id, + linkToken: created.linkToken, + expiresAt: created.expiresAt, + }); + } catch (error) { + ctx.log.error( + "Failed to create session share", + error instanceof Error ? error : undefined, + ); + res.status(500).json({ error: "Failed to create session share" }); + } + }); + + /** + * @openapi + * /plugin-api/session-sharing/host/{hostId}/active: + * get: + * summary: List active session shares for a host + * description: Returns active (non-revoked, non-expired) shares owned by the caller for the given host. + * tags: + * - Session Sharing + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: hostId + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: List of active shares + * 400: + * description: Invalid host id + * 500: + * description: Server error + */ + router.get( + "/host/:hostId/active", + requireUse, + async (req: Request, res: Response) => { + try { + const userId = actorOf(ctx); + const hostId = Number.parseInt(String(req.params.hostId), 10); + if (!hostId || Number.isNaN(hostId)) { + return res.status(400).json({ error: "Invalid host ID" }); + } + res.json({ + shares: await shares.findActiveSharesForHost(hostId, userId), + }); + } catch (error) { + ctx.log.error( + "Failed to list session shares", + error instanceof Error ? error : undefined, + ); + res.status(500).json({ error: "Failed to list session shares" }); + } + }, + ); + + /** + * @openapi + * /plugin-api/session-sharing/shared-with-me: + * get: + * summary: List live SSH sessions other users shared with the caller + * description: Active user shares aimed at the caller whose session is still live, in the shape of /open-tabs/active-sessions. + * tags: + * - Session Sharing + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Shared sessions + * 500: + * description: Server error + */ + router.get( + "/shared-with-me", + requireUse, + async (_req: Request, res: Response) => { + try { + const userId = actorOf(ctx); + const result = []; + for (const share of await shares.findSharesTargetingUser(userId)) { + if (share.protocol !== "ssh") continue; + const session = await live.getSession("ssh", share.sessionId); + if (!session?.isConnected) continue; + result.push({ + sessionId: session.id, + hostId: session.hostId, + hostName: session.hostName, + tabInstanceId: session.tabInstanceId, + isConnected: session.isConnected, + createdAt: session.createdAt, + isOwnSession: false, + sharedByUsername: await directory.username(share.ownerUserId), + permissionLevel: share.permissionLevel, + shareId: share.id, + }); + } + res.json(result); + } catch (error) { + ctx.log.error( + "Failed to list shared sessions", + error instanceof Error ? error : undefined, + ); + res.status(500).json({ error: "Failed to list shared sessions" }); + } + }, + ); + + /** + * @openapi + * /plugin-api/session-sharing/directory: + * get: + * summary: Users and roles to share with or invite + * tags: + * - Session Sharing + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Users (id, username) and roles (id, name, displayName) + * 500: + * description: Server error + */ + router.get("/directory", requireUse, async (_req: Request, res: Response) => { + try { + res.json({ + users: await directory.listUsers(), + roles: await directory.listRoles(), + }); + } catch (error) { + ctx.log.error( + "Failed to list users and roles", + error instanceof Error ? error : undefined, + ); + res.status(500).json({ error: "Failed to list users and roles" }); + } + }); + + /** + * @openapi + * /plugin-api/session-sharing/{shareId}: + * delete: + * summary: Revoke a session share + * description: Revokes a share. Owner or admin only. Best-effort kick of live SSH participants; remote desktop viewers are not force-disconnected. + * tags: + * - Session Sharing + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: shareId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Share revoked + * 403: + * description: Not authorized to revoke this share + * 404: + * description: Share not found + * 500: + * description: Server error + */ + router.delete( + "/:shareId", + requireUse, + async (req: Request, res: Response) => { + try { + const userId = actorOf(ctx); + const shareId = String(req.params.shareId); + const share = await shares.findById(shareId); + if (!share) { + return res.status(404).json({ error: "Share not found" }); + } + + let revoked = await shares.revoke(shareId, userId); + if (!revoked && (await directory.isAdmin(userId))) { + revoked = await shares.revokeAny(shareId); + } + if (!revoked) { + return res + .status(403) + .json({ error: "Not authorized to revoke this share" }); + } + + // guacamole-lite has no kick, so a revoked remote desktop link only + // blocks new resolves until the viewer's own socket ends. + if (share.protocol === "ssh") { + await live.ownerEndSession( + "ssh", + share.sessionId, + "Session share revoked by owner", + share.ownerUserId, + ); + } + + res.json({ success: true }); + } catch (error) { + ctx.log.error( + "Failed to revoke session share", + error instanceof Error ? error : undefined, + ); + res.status(500).json({ error: "Failed to revoke session share" }); + } + }, + ); + + /** + * @openapi + * /plugin-api/session-sharing/{shareId}/end: + * post: + * summary: End a shared session for all participants + * description: Owner-only. Terminates the underlying session and notifies joined participants. Remote desktop viewers are not force-disconnected. + * tags: + * - Session Sharing + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: shareId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Session ended + * 403: + * description: Not the owner of this share + * 404: + * description: Share not found + * 500: + * description: Server error + */ + router.post( + "/:shareId/end", + requireUse, + async (req: Request, res: Response) => { + try { + const userId = actorOf(ctx); + const share = await shares.findById(String(req.params.shareId)); + if (!share) { + return res.status(404).json({ error: "Share not found" }); + } + if (share.ownerUserId !== userId) { + return res.status(403).json({ error: "Not the owner of this share" }); + } + if (share.protocol === "ssh") { + await live.ownerEndSession( + "ssh", + share.sessionId, + "Session ended by owner", + ); + } + res.json({ success: true }); + } catch (error) { + ctx.log.error( + "Failed to end shared session", + error instanceof Error ? error : undefined, + ); + res.status(500).json({ error: "Failed to end shared session" }); + } + }, + ); +} diff --git a/plugins/session-sharing/src/backend/tables.ts b/plugins/session-sharing/src/backend/tables.ts new file mode 100644 index 000000000..515bbf18c --- /dev/null +++ b/plugins/session-sharing/src/backend/tables.ts @@ -0,0 +1,128 @@ +import { + adoptLegacyTable, + boolean, + defineTable, + id, + integer, + refHost, + refUser, + text, + timestamp, + varchar, +} from "@termix/plugin-sdk/db"; + +/* + * Adopted from core's session_shares, session_share_participants, + * collab_rooms and collab_room_members, so column and index names are the + * legacy ones and the rename carries every row across. + * + * Columns that point at a user or host but may be null or set null on delete, + * and the links between these four tables, are plain columns here: the SDK + * only references users and ssh_data, cascading. The adoption migrations keep + * the legacy FOREIGN KEY clauses by hand, as fleets does. + */ + +/** + * A grant to join one live session: a link for anonymous guests, a user, or + * a collab room's stage ("room"). + */ +export const shares = adoptLegacyTable( + "session_shares", + defineTable( + "shares", + { + id: varchar().primaryKey(), + hostId: refHost(), + ownerUserId: refUser(), + protocol: text().notNull(), + // The terminal's session id for SSH, guacd's connection id otherwise. + // Neither is a row, so there is no foreign key. + sessionId: varchar().notNull(), + tabInstanceId: text(), + shareType: text().notNull(), + targetUserId: varchar(), + linkToken: varchar().unique(), + permissionLevel: text().notNull().default("read-only"), + createdAt: timestamp().notNull().defaultNow(), + expiresAt: text().notNull(), + revokedAt: text(), + lastJoinedAt: text(), + joinCount: integer().notNull().default(0), + }, + { + indexes: [ + { name: "idx_session_shares_session_id", columns: ["sessionId"] }, + { name: "idx_session_shares_host_id", columns: ["hostId"] }, + ], + }, + ), +); + +/** Who joined through a share, for the owner's record. */ +export const shareParticipants = adoptLegacyTable( + "session_share_participants", + defineTable("share_participants", { + id: id(), + shareId: varchar().notNull(), + userId: varchar(), + guestLabel: text(), + joinedAt: timestamp().notNull().defaultNow(), + leftAt: text(), + }), +); + +/** + * A collaboration room: members watching one stage, the live session the + * current presenter shows through a "room" share. + */ +export const rooms = adoptLegacyTable( + "collab_rooms", + defineTable( + "rooms", + { + id: varchar().primaryKey(), + name: text().notNull(), + ownerUserId: refUser(), + persistent: boolean().notNull().default(false), + presenterUserId: varchar(), + stageProtocol: text(), + stageHostId: integer(), + stageShareId: varchar(), + guestLinkToken: varchar(), + createdAt: timestamp().notNull().defaultNow(), + endedAt: text(), + }, + { + indexes: [{ name: "idx_collab_rooms_owner", columns: ["ownerUserId"] }], + uniques: [ + { name: "idx_collab_rooms_guest_token", columns: ["guestLinkToken"] }, + ], + }, + ), +); + +export const roomMembers = adoptLegacyTable( + "collab_room_members", + defineTable( + "room_members", + { + id: id(), + roomId: varchar().notNull(), + userId: refUser(), + roomRole: text().notNull().default("member"), + addedBy: varchar(), + createdAt: timestamp().notNull().defaultNow(), + }, + { + uniques: [ + { + name: "idx_collab_room_members_room_user", + columns: ["roomId", "userId"], + }, + ], + indexes: [{ name: "idx_collab_room_members_user", columns: ["userId"] }], + }, + ), +); + +export const tables = [shares, shareParticipants, rooms, roomMembers]; diff --git a/src/ui/features/collab/CollabGuestView.tsx b/plugins/session-sharing/src/frontend/CollabGuestView.tsx similarity index 93% rename from src/ui/features/collab/CollabGuestView.tsx rename to plugins/session-sharing/src/frontend/CollabGuestView.tsx index 3a0d9d0fc..03c628876 100644 --- a/src/ui/features/collab/CollabGuestView.tsx +++ b/plugins/session-sharing/src/frontend/CollabGuestView.tsx @@ -1,15 +1,12 @@ import { useEffect, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "@termix/plugin-sdk/frontend"; import { AlertCircle, Eye, Presentation, Users } from "lucide-react"; -import { RemoteDisplay } from "@/features/remote-display/RemoteDisplay"; +import { RemoteDisplay } from "./shared"; import { GuestTerminalView, type SessionParticipantInfo, -} from "@/features/session-sharing/SharedSessionView"; -import { - resolveCollabGuestStage, - type CollabGuestStage, -} from "@/api/collab-api"; +} from "./SharedSessionView"; +import { resolveCollabGuestStage, type CollabGuestStage } from "./api"; const POLL_MS = 5000; diff --git a/src/ui/features/collab/CollabMembersSidebar.tsx b/plugins/session-sharing/src/frontend/CollabMembersSidebar.tsx similarity index 97% rename from src/ui/features/collab/CollabMembersSidebar.tsx rename to plugins/session-sharing/src/frontend/CollabMembersSidebar.tsx index f93ea0525..14e325459 100644 --- a/src/ui/features/collab/CollabMembersSidebar.tsx +++ b/plugins/session-sharing/src/frontend/CollabMembersSidebar.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "@termix/plugin-sdk/frontend"; import { Check, Crown, @@ -10,8 +10,8 @@ import { Users, X, } from "lucide-react"; -import { Badge } from "@/components/badge"; -import { Button } from "@/components/button"; +import { Badge } from "@termix/plugin-sdk/ui"; +import { Button } from "@termix/plugin-sdk/ui"; import { AlertDialog, AlertDialogAction, @@ -21,8 +21,8 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, -} from "@/components/alert-dialog"; -import type { CollabControlRequest, CollabRoomDetail } from "@/api/collab-api"; +} from "@termix/plugin-sdk/ui"; +import type { CollabControlRequest, CollabRoomDetail } from "./api"; interface CollabMembersSidebarProps { detail: CollabRoomDetail; diff --git a/src/ui/sidebar/CollabPanel.tsx b/plugins/session-sharing/src/frontend/CollabPanel.tsx similarity index 93% rename from src/ui/sidebar/CollabPanel.tsx rename to plugins/session-sharing/src/frontend/CollabPanel.tsx index 51557da97..385e5903f 100644 --- a/src/ui/sidebar/CollabPanel.tsx +++ b/plugins/session-sharing/src/frontend/CollabPanel.tsx @@ -1,23 +1,19 @@ import { useCallback, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "@termix/plugin-sdk/frontend"; import { toast } from "sonner"; import { Loader2, Plus, Presentation, RefreshCw } from "lucide-react"; -import { Button } from "@/components/button"; -import { Input } from "@/components/input"; -import { Badge } from "@/components/badge"; +import { Button } from "@termix/plugin-sdk/ui"; +import { Input } from "@termix/plugin-sdk/ui"; +import { Badge } from "@termix/plugin-sdk/ui"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, -} from "@/components/dialog"; -import { - createCollabRoom, - listCollabRooms, - type CollabRoom, -} from "@/api/collab-api"; -import { getErrorMessage } from "@/lib/error-message"; +} from "@termix/plugin-sdk/ui"; +import { createCollabRoom, listCollabRooms, type CollabRoom } from "./api"; +import { getErrorMessage } from "./shared"; export function CollabPanel({ onOpenRoom, diff --git a/src/ui/features/collab/CollabRoomTab.tsx b/plugins/session-sharing/src/frontend/CollabRoomTab.tsx similarity index 95% rename from src/ui/features/collab/CollabRoomTab.tsx rename to plugins/session-sharing/src/frontend/CollabRoomTab.tsx index e6d11b0f7..3f18f968e 100644 --- a/src/ui/features/collab/CollabRoomTab.tsx +++ b/plugins/session-sharing/src/frontend/CollabRoomTab.tsx @@ -1,6 +1,9 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { PluginComponent } from "@/plugin-host/component-registry"; -import { useTranslation } from "react-i18next"; +import { + useHosts, + useTranslation, + type PluginHostRecord, +} from "@termix/plugin-sdk/frontend"; import { toast } from "sonner"; import { AlertCircle, @@ -12,14 +15,14 @@ import { Square, Users, } from "lucide-react"; -import { Button } from "@/components/button"; +import { Button } from "@termix/plugin-sdk/ui"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, -} from "@/components/dialog"; +} from "@termix/plugin-sdk/ui"; import { AlertDialog, AlertDialogAction, @@ -29,18 +32,19 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, -} from "@/components/alert-dialog"; -import { Input } from "@/components/input"; +} from "@termix/plugin-sdk/ui"; +import { + Input, + PluginComponent, + resolveConnectionOrigin, +} from "@termix/plugin-sdk/ui"; import { CollabMembersSidebar } from "./CollabMembersSidebar"; import { RemoteDisplay, createRemoteSessionToken, -} from "@/features/remote-display/RemoteDisplay"; -import { getSSHHosts, getUserList, type SSHHostWithStatus } from "@/main-axios"; -import { getRoles } from "@/api/rbac-api"; -import type { Role } from "@/main-axios"; -import { pluginWsUrlForPath } from "@/lib/plugin-transport"; -import { getErrorMessage } from "@/lib/error-message"; + getErrorMessage, + wsUrlForPath, +} from "./shared"; import { deleteCollabRoom, endCollabRoom, @@ -54,10 +58,20 @@ import { setCollabGuestLink, setCollabStageControl, stopCollabStage, + getDirectory, type CollabRoomDetail, type CollabStage, -} from "@/api/collab-api"; -import { resolveConnectionOrigin } from "@/lib/connection-origin"; + type DirectoryRole, +} from "./api"; + +/** A host from the shell's list; the connection fields ride along by name. */ +type SSHHostWithStatus = PluginHostRecord & { + enableSsh?: boolean; + enableRdp?: boolean; + enableVnc?: boolean; + enableTelnet?: boolean; + connectionOrigin?: unknown; +}; const PING_INTERVAL_MS = 30000; const POLL_FALLBACK_MS = 15000; @@ -66,8 +80,7 @@ const POLL_FALLBACK_MS = 15000; // the jwt cookie the way every terminal WS connection does. async function roomEventsWsUrl(roomId: string): Promise { const { eventsWsPath } = await getCollabRoom(roomId); - const target = await pluginWsUrlForPath(eventsWsPath); - return target?.url ?? null; + return wsUrlForPath(eventsWsPath); } type PresentDraft = @@ -97,7 +110,8 @@ export function CollabRoomTab({ const [loadError, setLoadError] = useState(null); const [draft, setDraft] = useState(null); const [presentOpen, setPresentOpen] = useState(false); - const [presentLoading, setPresentLoading] = useState(false); + const { loaded: hostsLoaded } = useHosts(); + const presentLoading = !hostsLoaded; const [inviteOpen, setInviteOpen] = useState(false); const [membersOpen, setMembersOpen] = useState(true); const [endOpen, setEndOpen] = useState(false); @@ -112,14 +126,15 @@ export function CollabRoomTab({ >(null); const [hostSearch, setHostSearch] = useState(""); const [inviteSearch, setInviteSearch] = useState(""); - const [hosts, setHosts] = useState([]); + const { hosts: shellHosts } = useHosts(); + const hosts = shellHosts as SSHHostWithStatus[]; const [users, setUsers] = useState>( [], ); const [inviteSelection, setInviteSelection] = useState>( new Set(), ); - const [roles, setRoles] = useState([]); + const [roles, setRoles] = useState([]); const [roleSelection, setRoleSelection] = useState>(new Set()); const draftRef = useRef(null); draftRef.current = draft; @@ -306,18 +321,8 @@ export function CollabRoomTab({ } } - async function openPresentDialog() { + function openPresentDialog() { setPresentOpen(true); - if (hosts.length === 0) { - setPresentLoading(true); - try { - setHosts(await getSSHHosts({ includeStatus: false })); - } catch (error) { - toast.error(getErrorMessage(error)); - } finally { - setPresentLoading(false); - } - } } function choosePresent( @@ -347,7 +352,8 @@ export function CollabRoomTab({ Number(host.id), await resolveConnectionOrigin({ connectionType: protocol, - connectionOrigin: host.connectionOrigin, + connectionOrigin: host.connectionOrigin as + "local" | "remote" | undefined, }), protocol, ); @@ -423,17 +429,9 @@ export function CollabRoomTab({ setInviteSelection(new Set()); setRoleSelection(new Set()); try { - const [userResult, roleResult] = await Promise.all([ - getUserList(), - getRoles().catch(() => ({ roles: [] as Role[] })), - ]); - setUsers( - userResult.users.map((user) => ({ - id: user.userId, - username: user.username, - })), - ); - setRoles(roleResult.roles); + const directory = await getDirectory(); + setUsers(directory.users); + setRoles(directory.roles); } catch (error) { toast.error(getErrorMessage(error)); } diff --git a/src/ui/features/session-sharing/ShareSessionModal.tsx b/plugins/session-sharing/src/frontend/ShareSessionModal.tsx similarity index 97% rename from src/ui/features/session-sharing/ShareSessionModal.tsx rename to plugins/session-sharing/src/frontend/ShareSessionModal.tsx index 082c301c6..9e0fb61dd 100644 --- a/src/ui/features/session-sharing/ShareSessionModal.tsx +++ b/plugins/session-sharing/src/frontend/ShareSessionModal.tsx @@ -1,23 +1,22 @@ import { useEffect, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "@termix/plugin-sdk/frontend"; import { Check, Copy, Link2, Search, Shield, User, Users } from "lucide-react"; import { toast } from "sonner"; -import { Button } from "@/components/button"; -import { Input } from "@/components/input"; +import { Button } from "@termix/plugin-sdk/ui"; +import { Input } from "@termix/plugin-sdk/ui"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, -} from "@/components/dialog"; +} from "@termix/plugin-sdk/ui"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from "@/components/dropdown-menu"; -import { getUserList } from "@/main-axios"; +} from "@termix/plugin-sdk/ui"; import { createSessionShare, getActiveSessionShares, @@ -25,7 +24,8 @@ import { type SessionShareProtocol, type SessionSharePermissionLevel, type SessionShareRecord, -} from "@/api/session-sharing-api"; + getDirectory, +} from "./api"; const EXPIRY_PRESETS = [ { key: "oneHour", hours: 1 }, @@ -83,12 +83,12 @@ export function ShareSessionModal({ if (!open || sharesLoaded) return; setSharesLoaded(true); Promise.all([ - getUserList().catch(() => ({ users: [] })), + getDirectory().catch(() => ({ users: [] })), getActiveSessionShares(hostId).catch(() => ({ shares: [] })), ]).then(([usersRes, sharesRes]) => { setUsers( (usersRes.users ?? []).map((u) => ({ - id: String(u.userId), + id: String(u.id), username: u.username, })), ); @@ -313,7 +313,7 @@ export function ShareSessionModal({ setCustomHours(e.target.value)} className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" diff --git a/src/ui/features/session-sharing/SharedSessionView.tsx b/plugins/session-sharing/src/frontend/SharedSessionView.tsx similarity index 91% rename from src/ui/features/session-sharing/SharedSessionView.tsx rename to plugins/session-sharing/src/frontend/SharedSessionView.tsx index 69eb42f0e..3bf85eff9 100644 --- a/src/ui/features/session-sharing/SharedSessionView.tsx +++ b/plugins/session-sharing/src/frontend/SharedSessionView.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "@termix/plugin-sdk/frontend"; import { useXTerm } from "react-xtermjs"; import { FitAddon } from "@xterm/addon-fit"; import { AlertCircle, Eye, Users } from "lucide-react"; @@ -7,10 +7,8 @@ import { resolveShareLink, type ResolvedShareLink, type ShareLinkErrorKind, -} from "@/api/session-sharing-api"; -import { SimpleLoader } from "@/lib/SimpleLoader.tsx"; -import { pluginWsUrlForPath } from "@/lib/plugin-transport"; -import { RemoteDisplay } from "@/features/remote-display/RemoteDisplay"; +} from "./api"; +import { Loader, RemoteDisplay, wsUrlForPath } from "./shared"; const PING_INTERVAL_MS = 30000; @@ -20,15 +18,13 @@ interface TerminalWsMessage { [key: string]: unknown; } -// Mirrors Terminal.tsx's baseWsUrl construction (dev/electron/embedded/prod). -// Duplicated rather than extracted from that file to avoid touching it here. -// A shared session link is always resolved against the desktop app's -// embedded local backend -- joining a session hosted on someone else's -// remote server isn't supported from the desktop app today. +// A shared session link always resolves against the desktop app's embedded +// local backend: joining a session on someone else's remote server is not +// supported from the desktop app. async function resolveTerminalWsUrl(wsPath: string): Promise { - const target = await pluginWsUrlForPath(wsPath); - if (!target) throw new Error("No terminal endpoint is available"); - return target.url; + const url = await wsUrlForPath(wsPath); + if (!url) throw new Error("No terminal endpoint is available"); + return url; } export interface SessionParticipantInfo { @@ -332,12 +328,7 @@ export default function SharedSessionView() { style={{ backgroundColor: "var(--bg-base)" }} >
- {loading && ( - - )} + {loading && } {!loading && error && ( ( + request: () => Promise<{ data: T }>, + fallback: string, +): Promise { + try { + return (await request()).data; + } catch (error) { + throw apiError(error, fallback); + } +} + +// --------------------------------------------------------------------------- +// Guest share links +// --------------------------------------------------------------------------- + +export interface ResolvedShareLink { + protocol: "ssh" | "rdp" | "vnc" | "telnet"; + permissionLevel: "read-only" | "read-write"; + wsPath: string; + connectParams?: { token: string }; +} + +export type ShareLinkErrorKind = "not-found" | "rate-limited" | "unknown"; + +export class ShareLinkError extends Error { + constructor( + message: string, + public readonly kind: ShareLinkErrorKind, + ) { + super(message); + this.name = "ShareLinkError"; + } +} + +export async function resolveShareLink( + linkToken: string, +): Promise { + try { + const response = await api().get( + `/resolve/${encodeURIComponent(linkToken)}`, + ); + return response.data; + } catch (error) { + const status = statusOf(error); + if (status === 404) { + throw new ShareLinkError( + "Share link is invalid, expired, or revoked", + "not-found", + ); + } + if (status === 429) { + throw new ShareLinkError( + "Too many attempts, please try again shortly", + "rate-limited", + ); + } + throw new ShareLinkError("Failed to resolve share link", "unknown"); + } +} + +// --------------------------------------------------------------------------- +// Owner-side shares +// --------------------------------------------------------------------------- + +export type SessionShareProtocol = "ssh" | "rdp" | "vnc" | "telnet"; +export type SessionShareType = "link" | "user"; +export type SessionSharePermissionLevel = "read-only" | "read-write"; + +export interface SessionShareRecord { + id: string; + hostId: number; + ownerUserId: string; + protocol: SessionShareProtocol; + sessionId: string; + tabInstanceId: string | null; + shareType: SessionShareType; + targetUserId: string | null; + linkToken: string | null; + permissionLevel: SessionSharePermissionLevel; + createdAt: string; + expiresAt: string; + revokedAt: string | null; + lastJoinedAt: string | null; + joinCount: number; +} + +export interface CreateSessionShareRequest { + hostId: number; + sessionId: string; + tabInstanceId?: string; + protocol: SessionShareProtocol; + shareType: SessionShareType; + targetUserId?: string; + permissionLevel: SessionSharePermissionLevel; + expiryHours?: number; +} + +export interface CreateSessionShareResponse { + shareId: string; + linkToken: string | null; + expiresAt: string; +} + +export function createSessionShare( + request: CreateSessionShareRequest, +): Promise { + return call( + () => api().post("/create", request), + "Failed to create session share", + ); +} + +export function getActiveSessionShares( + hostId: number, +): Promise<{ shares: SessionShareRecord[] }> { + return call( + () => api().get<{ shares: SessionShareRecord[] }>(`/host/${hostId}/active`), + "Failed to fetch active session shares", + ); +} + +export function revokeSessionShare(shareId: string): Promise { + return call( + () => api().delete(`/${encodeURIComponent(shareId)}`), + "Failed to revoke session share", + ); +} + +export interface DirectoryUser { + id: string; + username: string; +} + +export interface DirectoryRole { + id: number; + name: string; + displayName: string | null; +} + +/** Users and roles to share with or invite. */ +export function getDirectory(): Promise<{ + users: DirectoryUser[]; + roles: DirectoryRole[]; +}> { + return call( + () => + api().get<{ users: DirectoryUser[]; roles: DirectoryRole[] }>( + "/directory", + ), + "Failed to list users and roles", + ); +} + +/** Live sessions other users shared with the caller, as active-session rows. */ +export function getSharedWithMe(): Promise { + return call( + () => api().get("/shared-with-me"), + "Failed to list shared sessions", + ); +} + +// --------------------------------------------------------------------------- +// Collab rooms +// --------------------------------------------------------------------------- + +export interface CollabRoom { + id: string; + name: string; + ownerUserId: string; + persistent: boolean; + presenterUserId: string | null; + stageProtocol: string | null; + stageHostId: number | null; + stageShareId: string | null; + guestLinkEnabled: boolean; + createdAt: string; + endedAt: string | null; +} + +export interface CollabRoomMember { + userId: string; + username: string; + roomRole: string; + createdAt: string; +} + +export interface CollabOnlineUser { + userId: string; + username: string; +} + +export interface CollabStage { + presenterUserId: string | null; + protocol: "ssh" | "rdp" | "vnc" | "telnet" | null; + hostId: number | null; + shareId: string | null; + sessionId?: string; + controllerUserId?: string | null; + connectParams?: { token: string }; +} + +export interface CollabControlRequest { + userId: string; + username: string; + requestedAt: string; +} + +export interface CollabRoomDetail { + room: CollabRoom; + me: string; + isHost: boolean; + members: CollabRoomMember[]; + online: CollabOnlineUser[]; + stage: CollabStage; + controllerUserId: string | null; + controlRequests: CollabControlRequest[]; + /** Where live room events are served, as /plugin-ws//. */ + eventsWsPath: string; +} + +const room = (roomId: string, suffix = "") => + `/rooms/${encodeURIComponent(roomId)}${suffix}`; + +export function listCollabRooms(): Promise<{ rooms: CollabRoom[] }> { + return call( + () => api().get<{ rooms: CollabRoom[] }>("/rooms"), + "Failed to list rooms", + ); +} + +export function createCollabRoom( + name: string, + persistent: boolean, +): Promise<{ room: CollabRoom }> { + return call( + () => api().post<{ room: CollabRoom }>("/rooms", { name, persistent }), + "Failed to create room", + ); +} + +export function getCollabRoom(roomId: string): Promise { + return call( + () => api().get(room(roomId)), + "Failed to get room", + ); +} + +export async function inviteCollabMembers( + roomId: string, + targets: { userIds?: string[]; roleIds?: number[] }, +): Promise { + await call( + () => api().post(room(roomId, "/members"), targets), + "Failed to invite members", + ); +} + +export async function removeCollabMember( + roomId: string, + userId: string, +): Promise { + await call( + () => api().delete(room(roomId, `/members/${encodeURIComponent(userId)}`)), + "Failed to remove member", + ); +} + +export function presentCollabStage( + roomId: string, + input: { protocol: string; sessionId: string; hostId: number }, +): Promise<{ stage: CollabStage }> { + return call( + () => api().post<{ stage: CollabStage }>(room(roomId, "/present"), input), + "Failed to start presenting", + ); +} + +export async function stopCollabStage(roomId: string): Promise { + await call( + () => api().post(room(roomId, "/stop")), + "Failed to stop presenting", + ); +} + +export function getCollabStage( + roomId: string, +): Promise<{ stage: CollabStage | null }> { + return call( + () => api().get<{ stage: CollabStage | null }>(room(roomId, "/stage")), + "Failed to resolve stage", + ); +} + +export async function setCollabStageControl( + roomId: string, + userId: string | null, +): Promise { + await call( + () => api().post(room(roomId, "/control"), { userId }), + "Failed to change stage control", + ); +} + +export function requestCollabStageControl( + roomId: string, +): Promise<{ request: CollabControlRequest }> { + return call( + () => + api().post<{ request: CollabControlRequest }>( + room(roomId, "/control/request"), + ), + "Failed to request control", + ); +} + +export function listCollabControlRequests( + roomId: string, +): Promise<{ requests: CollabControlRequest[] }> { + return call( + () => + api().get<{ requests: CollabControlRequest[] }>( + room(roomId, "/control/requests"), + ), + "Failed to list control requests", + ); +} + +export async function dismissCollabControlRequest( + roomId: string, + userId: string, +): Promise { + await call( + () => + api().delete( + room(roomId, `/control/requests/${encodeURIComponent(userId)}`), + ), + "Failed to dismiss control request", + ); +} + +export async function endCollabRoom(roomId: string): Promise { + await call(() => api().post(room(roomId, "/end")), "Failed to end room"); +} + +export async function deleteCollabRoom(roomId: string): Promise { + await call(() => api().delete(room(roomId)), "Failed to delete room"); +} + +export function setCollabGuestLink( + roomId: string, + enabled: boolean, +): Promise<{ guestLinkToken: string | null }> { + return call( + () => + api().post<{ guestLinkToken: string | null }>( + room(roomId, "/guest-link"), + { enabled }, + ), + "Failed to update guest link", + ); +} + +export interface CollabGuestStage { + protocol: "ssh" | "rdp" | "vnc" | "telnet"; + shareId: string; + wsPath?: string; + connectParams?: { token: string }; +} + +/** Anonymous: guests poll this to follow the presenter. Throws on 404/429. */ +export async function resolveCollabGuestStage( + token: string, +): Promise<{ roomName: string; stage: CollabGuestStage | null }> { + const response = await api().get<{ + roomName: string; + stage: CollabGuestStage | null; + }>(`/guest/${encodeURIComponent(token)}`); + return response.data; +} diff --git a/plugins/session-sharing/src/frontend/index.tsx b/plugins/session-sharing/src/frontend/index.tsx new file mode 100644 index 000000000..6726ca46e --- /dev/null +++ b/plugins/session-sharing/src/frontend/index.tsx @@ -0,0 +1,233 @@ +import { useEffect, type ComponentType } from "react"; +import { Presentation, Share2 } from "lucide-react"; +import { toast } from "sonner"; +import { + useCurrentUser, + usePermission, + type PanelProps, + type StandaloneViewProps, + type TabProps, + type TermixApp, +} from "@termix/plugin-sdk/frontend"; +import { bindApi, getSharedWithMe, listCollabRooms } from "./api"; +import CollabGuestView from "./CollabGuestView"; +import { CollabPanel } from "./CollabPanel"; +import { CollabRoomTab } from "./CollabRoomTab"; +import SharedSessionView from "./SharedSessionView"; +import { + ShareDialogHost, + closeShareDialog, + openShareDialog, + type ShareTarget, +} from "./share-dialog"; +import { REMOTE_DISPLAY_SLOT } from "./shared"; + +const COLLAB = "collab"; +const SHARE_ACTION = "session-sharing.share"; +const SHARED_WITH_ME_ACTION = "sessions.sharedWithMe"; +const SEEN_ROOMS_KEY = "termix:collab-rooms-seen"; +const INVITE_POLL_MS = 60_000; + +/** `?view=shared` and `?view=collab-guest`, the two anonymous guest links. */ +function GuestView({ view }: StandaloneViewProps) { + return view === "shared" ? : ; +} + +function CollabRoomTabView({ tab, isVisible }: TabProps) { + const roomId = tab.data?.roomId; + return ( + + ); +} + +function CollabPanelView({ shell }: PanelProps) { + return ( + + shell.openTab(null, COLLAB, { + label: room.name, + forceNewTab: true, + data: { roomId: room.id }, + }) + } + /> + ); +} + +/** + * Rooms are discovered by polling, so a room this browser has never shown + * gets one toast with an Open action. The first poll after login only + * records what already exists. + */ +function createInviteWatcher(app: TermixApp): ComponentType { + return function InviteWatcher() { + const user = useCurrentUser(); + const allowed = usePermission("use"); + const userId = user?.userId; + + useEffect(() => { + if (!userId || !allowed) return; + let cancelled = false; + const check = async () => { + try { + const { rooms = [] } = await listCollabRooms(); + if (cancelled) return; + let seen: string[] = []; + try { + seen = JSON.parse(localStorage.getItem(SEEN_ROOMS_KEY) ?? "[]"); + } catch { + seen = []; + } + const seenSet = new Set(seen); + const fresh = rooms.filter((room) => !seenSet.has(room.id)); + if (fresh.length === 0) return; + localStorage.setItem( + SEEN_ROOMS_KEY, + JSON.stringify([...seenSet, ...fresh.map((room) => room.id)]), + ); + if (seen.length === 0) return; + for (const room of fresh) { + if (room.ownerUserId === userId) continue; + toast(app.t("collab.invitedTo", { name: room.name }), { + action: { + label: app.t("collab.openRoom"), + onClick: () => app.tabs.openRailView(COLLAB), + }, + }); + } + } catch { + /* next poll */ + } + }; + void check(); + const timer = setInterval(() => void check(), INVITE_POLL_MS); + return () => { + cancelled = true; + clearInterval(timer); + }; + }, [userId, allowed]); + + return null; + }; +} + +/** A terminal toolbar action is invoked with the terminal's slot API. */ +interface TerminalSlotApi { + getShareTarget?: () => ShareTarget | null; +} + +/** A remote desktop toolbar action is invoked with the session it shows. */ +interface RemoteDesktopToolbarContext { + hostId: number; + sessionId: string | null; + protocol: "rdp" | "vnc" | "telnet"; + tabInstanceId?: string; +} + +function isRemoteDesktopContext( + value: unknown, +): value is RemoteDesktopToolbarContext { + return ( + !!value && + typeof value === "object" && + "protocol" in value && + "hostId" in value + ); +} + +export function activate(app: TermixApp): void { + bindApi(app.api); + app.onDispose(() => { + closeShareDialog(); + bindApi(null); + }); + + // Rooms and share links draw remote desktop streams through this slot. + app.declareActionSlot({ id: REMOTE_DISPLAY_SLOT, accepts: ["component"] }); + + // Guest pages open the tab type's standalone view by ?view= name. + const guestViews = { + standalone: GuestView, + standaloneViews: ["shared", "collab-guest"], + }; + + if (app.guest) { + app.registerTab(COLLAB, () => null, { hostless: true, ...guestViews }); + return; + } + + app.registerRailItem({ + id: COLLAB, + icon: Presentation, + titleKey: "nav.collab", + kind: "panel", + after: "connections", + separatorAfter: true, + permission: "use", + }); + app.registerPanel(COLLAB, CollabPanelView); + app.registerTab(COLLAB, CollabRoomTabView, { + icon: Presentation, + titleKey: "nav.collab", + hostless: true, + multiInstance: true, + ...guestViews, + }); + + app.registerAction( + SHARE_ACTION, + (context: unknown) => { + let target: ShareTarget | null = null; + if (isRemoteDesktopContext(context)) { + target = context.sessionId + ? { + hostId: context.hostId, + sessionId: context.sessionId, + protocol: context.protocol, + tabInstanceId: context.tabInstanceId, + } + : null; + } else { + target = (context as TerminalSlotApi)?.getShareTarget?.() ?? null; + } + if (target) openShareDialog(target); + else toast.error(app.t("sessionSharing.notReadyToShare")); + }, + { permission: "use" }, + ); + for (const slot of ["terminal.toolbar", "remote-desktop.toolbar"]) { + app.registerSlotContribution(slot, { + actionId: SHARE_ACTION, + titleKey: "sessionSharing.shareButton", + icon: Share2, + kind: "button", + }); + } + + app.registerSlotContribution("shell.overlay", { + actionId: "session-sharing.shareDialog", + titleKey: "sessionSharing.shareButton", + kind: "component", + component: ShareDialogHost as ComponentType>, + }); + app.registerSlotContribution("shell.overlay", { + actionId: "session-sharing.inviteWatcher", + titleKey: "nav.collab", + kind: "component", + component: createInviteWatcher(app) as ComponentType< + Record + >, + }); + + // Core's active connections list asks for sessions shared with the user. + app.registerAction(SHARED_WITH_ME_ACTION, () => getSharedWithMe(), { + permission: "use", + }); +} + +export function deactivate(): void { + closeShareDialog(); +} diff --git a/plugins/session-sharing/src/frontend/share-dialog.tsx b/plugins/session-sharing/src/frontend/share-dialog.tsx new file mode 100644 index 000000000..9f55a2905 --- /dev/null +++ b/plugins/session-sharing/src/frontend/share-dialog.tsx @@ -0,0 +1,50 @@ +import { useSyncExternalStore } from "react"; +import { ShareSessionModal } from "./ShareSessionModal"; +import type { SessionShareProtocol } from "./api"; + +/** What the share dialog needs to share one live session. */ +export interface ShareTarget { + hostId: number; + sessionId: string; + protocol: SessionShareProtocol; + tabInstanceId?: string; +} + +let current: ShareTarget | null = null; +const listeners = new Set<() => void>(); + +function emit(): void { + for (const listener of listeners) listener(); +} + +/** Opens the share dialog, which the shell overlay slot keeps mounted. */ +export function openShareDialog(target: ShareTarget): void { + current = target; + emit(); +} + +export function closeShareDialog(): void { + current = null; + emit(); +} + +function subscribe(listener: () => void): () => void { + listeners.add(listener); + return () => listeners.delete(listener); +} + +/** Rendered once in "shell.overlay"; shows the dialog when a toolbar asks. */ +export function ShareDialogHost() { + const target = useSyncExternalStore(subscribe, () => current); + if (!target) return null; + return ( + + ); +} diff --git a/plugins/session-sharing/src/frontend/shared.tsx b/plugins/session-sharing/src/frontend/shared.tsx new file mode 100644 index 000000000..99212881a --- /dev/null +++ b/plugins/session-sharing/src/frontend/shared.tsx @@ -0,0 +1,90 @@ +import { useMemo } from "react"; +import { + invokeAction, + useSlotContributions, + useTranslation, +} from "@termix/plugin-sdk/frontend"; +import { pluginWsUrl } from "@termix/plugin-sdk/ui"; + +/** + * A remote desktop stream drawn by whichever plugin provides one. Rooms and + * share links show RDP, VNC and Telnet without knowing how they are drawn: + * the provider contributes a component to this slot and answers the token + * action below. + */ +export const REMOTE_DISPLAY_SLOT = "session.remoteDisplay"; +export const REMOTE_SESSION_TOKEN_ACTION = "session.remoteDisplay.token"; + +export interface RemoteDisplayProps { + token: string; + protocol: string; + isVisible: boolean; + onConnect?: () => void; + onError?: (error: string) => void; +} + +export function RemoteDisplay(props: RemoteDisplayProps) { + const { t } = useTranslation(); + const contributions = useSlotContributions(REMOTE_DISPLAY_SLOT); + const provider = useMemo( + () => + contributions.find( + (contribution) => + contribution.kind === "component" && contribution.component, + ), + [contributions], + ); + if (!provider?.component) { + return ( +
+ {t("sessionSharing.remoteDisplayUnavailable")} +
+ ); + } + const Display = provider.component; + return )} />; +} + +export interface RemoteSessionToken { + token: string; + connectionId: string; +} + +/** Mints a session token for presenting a host, through the provider. */ +export async function createRemoteSessionToken( + hostId: number, + origin: unknown, + protocol: string, +): Promise { + return ((await invokeAction( + REMOTE_SESSION_TOKEN_ACTION, + hostId, + origin, + protocol, + )) ?? null) as RemoteSessionToken | null; +} + +/** A socket URL for a /plugin-ws//?query path the server handed out. */ +export async function wsUrlForPath(wsPath: string): Promise { + const [pathname, query] = wsPath.split("?", 2); + const match = /^\/plugin-ws\/([^/]+)(\/.*)$/.exec(pathname); + if (!match) return null; + const target = await pluginWsUrl(match[1], match[2]); + if (!target) return null; + if (!query) return target.url; + return `${target.url}${target.url.includes("?") ? "&" : "?"}${query}`; +} + +export function getErrorMessage(error: unknown): string { + if (error instanceof Error) return error.message; + return typeof error === "string" ? error : String(error); +} + +export function Loader({ message }: { message: string }) { + return ( +
+
+

{message}

+
+ ); +} diff --git a/plugins/session-sharing/tests/backend/activate.test.ts b/plugins/session-sharing/tests/backend/activate.test.ts new file mode 100644 index 000000000..eaa2d4155 --- /dev/null +++ b/plugins/session-sharing/tests/backend/activate.test.ts @@ -0,0 +1,69 @@ +import express from "express"; +import { afterEach, describe, expect, it } from "vitest"; +import { PluginCapabilityError } from "@termix/plugin-sdk/backend"; +import { + createMockCtx, + createTestDb, + type TestDb, +} from "@termix/plugin-sdk/testing"; +import { activate } from "../../src/backend/index.js"; +import { manifest, pluginDir, startServer, type TestServer } from "./helpers"; + +let db: TestDb | null = null; +let server: TestServer | null = null; + +afterEach(async () => { + db?.close(); + db = null; + await server?.close(); + server = null; +}); + +describe("session-sharing activate", () => { + it("provides sessions.sharing and the guest resolver", async () => { + server = await startServer(); + expect(server.mock.services.get("sessions.sharing")).toBeDefined(); + expect(typeof server.guests.resolve).toBe("function"); + expect(server.mock.httpRouters).toEqual([ + { public: ["/resolve/:linkToken", "/guest/:token"] }, + ]); + }); + + it.each(["db:own", "network:serve"])( + "fails closed without %s", + async (capability) => { + db = await createTestDb(pluginDir); + const mock = createMockCtx({ + pluginId: manifest.id, + manifest, + capabilities: manifest.capabilities.filter((c) => c !== capability), + db: db.database, + router: () => express.Router(), + }); + await expect(activate(mock.ctx)).rejects.toBeInstanceOf( + PluginCapabilityError, + ); + for (const dispose of [...mock.disposals].reverse()) await dispose(); + }, + ); + + it("cannot check a share target's host access without hosts:read", async () => { + db = await createTestDb(pluginDir); + db.sqlite.exec( + "INSERT INTO users (id, username) VALUES ('alice', 'alice'), ('bob', 'bob'); INSERT INTO ssh_data (id) VALUES (1)", + ); + const mock = createMockCtx({ + pluginId: manifest.id, + manifest, + capabilities: manifest.capabilities.filter((c) => c !== "hosts:read"), + db: db.database, + router: () => express.Router(), + actor: "bob", + }); + await activate(mock.ctx); + await expect( + mock.ctx.asUser("bob", () => mock.ctx.hosts.checkAccess(1, "connect")), + ).rejects.toBeInstanceOf(PluginCapabilityError); + for (const dispose of [...mock.disposals].reverse()) await dispose(); + }); +}); diff --git a/plugins/session-sharing/tests/backend/helpers.ts b/plugins/session-sharing/tests/backend/helpers.ts new file mode 100644 index 000000000..711987252 --- /dev/null +++ b/plugins/session-sharing/tests/backend/helpers.ts @@ -0,0 +1,182 @@ +import http from "node:http"; +import type { AddressInfo } from "node:net"; +import { fileURLToPath } from "node:url"; +import express, { type Router } from "express"; +import { + createMockCtx, + createTestDb, + type MockPluginContext, + type TestDb, +} from "@termix/plugin-sdk/testing"; +import type { PluginManifest } from "@termix/plugin-sdk/manifest"; +import manifestJson from "../../manifest.json"; +import type { LiveSessionInfo } from "../../src/backend/live.js"; +import type { + SessionGuestsV1, + SessionSharingV1, +} from "../../src/backend/services.js"; + +export const pluginDir = fileURLToPath(new URL("../..", import.meta.url)); +export const manifest = manifestJson as unknown as PluginManifest; + +/** A stand-in for the terminal's "ssh" sessions.live provider. */ +export function createFakeLive() { + const sessions = new Map(); + const calls: Array<[string, ...unknown[]]> = []; + return { + sessions, + calls, + addSession(id: string, userId: string, hostId = 1) { + sessions.set(id, { + id, + userId, + hostId, + hostName: `host-${hostId}`, + isConnected: true, + createdAt: 1, + tabInstanceId: `tab-${id}`, + }); + }, + provider: { + getSession: (id: string) => sessions.get(id) ?? null, + ownerEndSession: (...args: unknown[]) => { + calls.push(["ownerEndSession", ...args]); + }, + disconnectParticipants: (...args: unknown[]) => { + calls.push(["disconnectParticipants", ...args]); + return 0; + }, + setRoomShareControl: (...args: unknown[]) => { + calls.push(["setRoomShareControl", ...args]); + }, + }, + }; +} + +export interface TestServer { + db: TestDb; + mock: MockPluginContext; + live: ReturnType; + sharing: SessionSharingV1; + guests: SessionGuestsV1; + request: ( + method: string, + path: string, + options?: { user?: string | null; body?: unknown }, + // Route bodies vary per test; asserting on them is the point. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) => Promise<{ status: number; body: any }>; + close: () => Promise; +} + +/** + * The plugin activated against a real in-memory database, its router mounted + * the way core mounts it: JSON parsed and the acting user set from the + * request (none for a guest), everything else left to the plugin. + */ +export async function startServer( + options: { + permissions?: string[]; + users?: string[]; + admins?: string[]; + hostIds?: number[]; + /** Hosts ctx.hosts.checkAccess grants. Defaults to every host. */ + accessibleHostIds?: number[]; + settings?: Record; + } = {}, +): Promise { + const db = await createTestDb(pluginDir); + for (const user of options.users ?? ["alice", "bob", "carol"]) { + db.sqlite + .prepare("INSERT INTO users (id, username, is_admin) VALUES (?, ?, ?)") + .run(user, user, options.admins?.includes(user) ? 1 : 0); + } + const hostIds = options.hostIds ?? [1, 2]; + for (const id of hostIds) { + db.sqlite.prepare("INSERT INTO ssh_data (id) VALUES (?)").run(id); + } + + const live = createFakeLive(); + let router: Router | null = null; + const mock = createMockCtx({ + pluginId: manifest.id, + manifest, + capabilities: manifest.capabilities, + db: db.database, + router: () => (router = express.Router()), + permissions: options.permissions ?? ["session-sharing.use"], + settings: options.settings, + hosts: (options.accessibleHostIds ?? hostIds).map((id) => ({ + id, + userId: "alice", + name: `host-${id}`, + ip: "10.0.0.1", + port: 22, + username: "root", + tags: [], + folder: null, + authType: "password", + })) as never, + services: { "sessions.live#ssh": live.provider }, + }); + + const { activate } = await import("../../src/backend/index.js"); + await activate(mock.ctx); + + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + mock.setActor(req.header("x-test-user") || undefined); + next(); + }); + app.use((req, res, next) => router!(req, res, next)); + + const server = http.createServer(app); + await new Promise((resolve) => server.listen(0, resolve)); + const { port } = server.address() as AddressInfo; + + return { + db, + mock, + live, + sharing: mock.services.get("sessions.sharing") as SessionSharingV1, + guests: mock.ctx.registry.consume( + "sessions.sharing.guests", + )!, + async request(method, path, { user = "alice", body: given } = {}) { + const body = method === "GET" ? undefined : given; + const response = await fetch(`http://127.0.0.1:${port}${path}`, { + method, + headers: { + ...(user ? { "x-test-user": user } : {}), + ...(body !== undefined ? { "content-type": "application/json" } : {}), + }, + body: body !== undefined ? JSON.stringify(body) : undefined, + }); + const text = await response.text(); + return { status: response.status, body: text ? JSON.parse(text) : null }; + }, + async close() { + await new Promise((resolve) => server.close(() => resolve())); + for (const dispose of [...mock.disposals].reverse()) { + await Promise.resolve(dispose()).catch(() => {}); + } + db.close(); + }, + }; +} + +/** Runs a service call the way a consumer's handle does: as that user. */ +export function asUser( + server: TestServer, + userId: string, + fn: () => Promise, +): Promise { + return server.mock.ctx.asUser(userId, fn); +} + +export function expireShare(server: TestServer, shareId: string): void { + server.db.sqlite + .prepare("UPDATE p_session_sharing_shares SET expires_at = ? WHERE id = ?") + .run(new Date(Date.now() - 60_000).toISOString(), shareId); +} diff --git a/plugins/session-sharing/tests/backend/migrations.test.ts b/plugins/session-sharing/tests/backend/migrations.test.ts new file mode 100644 index 000000000..b69bc1696 --- /dev/null +++ b/plugins/session-sharing/tests/backend/migrations.test.ts @@ -0,0 +1,222 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { createTestDb, type TestDb } from "@termix/plugin-sdk/testing"; +import { pluginDir } from "./helpers"; + +// The four tables as core's SQLite bootstrap created them before 2.9.0, +// including the guest token column and its index added later. +const LEGACY_DDL = ` + CREATE TABLE session_shares ( + id TEXT PRIMARY KEY, + host_id INTEGER NOT NULL, + owner_user_id TEXT NOT NULL, + protocol TEXT NOT NULL, + session_id TEXT NOT NULL, + tab_instance_id TEXT, + share_type TEXT NOT NULL, + target_user_id TEXT, + link_token TEXT UNIQUE, + permission_level TEXT NOT NULL DEFAULT 'read-only', + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires_at TEXT NOT NULL, + revoked_at TEXT, + last_joined_at TEXT, + join_count INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE, + FOREIGN KEY (owner_user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (target_user_id) REFERENCES users (id) ON DELETE CASCADE + ); + CREATE TABLE session_share_participants ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + share_id TEXT NOT NULL, + user_id TEXT, + guest_label TEXT, + joined_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + left_at TEXT, + FOREIGN KEY (share_id) REFERENCES session_shares (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + CREATE TABLE collab_rooms ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + owner_user_id TEXT NOT NULL, + persistent INTEGER NOT NULL DEFAULT 0, + presenter_user_id TEXT, + stage_protocol TEXT, + stage_host_id INTEGER, + stage_share_id TEXT, + guest_link_token TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + ended_at TEXT, + FOREIGN KEY (owner_user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (presenter_user_id) REFERENCES users (id) ON DELETE SET NULL, + FOREIGN KEY (stage_host_id) REFERENCES ssh_data (id) ON DELETE SET NULL, + FOREIGN KEY (stage_share_id) REFERENCES session_shares (id) ON DELETE SET NULL + ); + CREATE TABLE collab_room_members ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + room_id TEXT NOT NULL, + user_id TEXT NOT NULL, + room_role TEXT NOT NULL DEFAULT 'member', + added_by TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (room_id, user_id), + FOREIGN KEY (room_id) REFERENCES collab_rooms (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (added_by) REFERENCES users (id) ON DELETE SET NULL + ); + CREATE INDEX idx_session_shares_session_id ON session_shares (session_id); + CREATE INDEX idx_session_shares_host_id ON session_shares (host_id); + CREATE UNIQUE INDEX idx_collab_rooms_guest_token ON collab_rooms (guest_link_token); +`; + +const TABLES = { + session_shares: "p_session_sharing_shares", + session_share_participants: "p_session_sharing_share_participants", + collab_rooms: "p_session_sharing_rooms", + collab_room_members: "p_session_sharing_room_members", +}; + +let db: TestDb | null = null; + +afterEach(() => { + db?.close(); + db = null; +}); + +const exists = (type: "table" | "index", name: string) => + !!db!.sqlite + .prepare("SELECT name FROM sqlite_master WHERE type = ? AND name = ?") + .get(type, name); + +function seedUsersAndHost(sqlite: TestDb["sqlite"]) { + sqlite.exec( + "INSERT INTO users (id, username) VALUES ('alice', 'alice'), ('bob', 'bob')", + ); + sqlite.exec("INSERT INTO ssh_data (id) VALUES (7)"); +} + +describe("adopting the session sharing tables", () => { + it("keeps every row and index when the legacy tables are there", async () => { + db = await createTestDb(pluginDir, { + before: (sqlite) => { + seedUsersAndHost(sqlite); + sqlite.exec(LEGACY_DDL); + sqlite.exec(` + INSERT INTO session_shares (id, host_id, owner_user_id, protocol, session_id, share_type, link_token, permission_level, expires_at, join_count) + VALUES ('share-1', 7, 'alice', 'ssh', 'sess-1', 'link', 'tok-1', 'read-write', '2999-01-01T00:00:00.000Z', 3); + INSERT INTO session_share_participants (share_id, user_id, guest_label) VALUES ('share-1', NULL, 'Guest'); + INSERT INTO collab_rooms (id, name, owner_user_id, persistent, stage_share_id, stage_host_id, guest_link_token) + VALUES ('room-1', 'Standup', 'alice', 1, 'share-1', 7, 'guest-tok'); + INSERT INTO collab_room_members (room_id, user_id, room_role) VALUES ('room-1', 'alice', 'host'), ('room-1', 'bob', 'member'); + `); + }, + }); + + expect(db.applied).toEqual(["0001_adopt_session_sharing"]); + for (const [legacy, adopted] of Object.entries(TABLES)) { + expect(exists("table", legacy)).toBe(false); + expect(exists("table", adopted)).toBe(true); + } + + expect( + db.sqlite + .prepare( + "SELECT id, link_token, permission_level, join_count FROM p_session_sharing_shares", + ) + .all(), + ).toEqual([ + { + id: "share-1", + link_token: "tok-1", + permission_level: "read-write", + join_count: 3, + }, + ]); + expect( + db.sqlite + .prepare( + "SELECT name, persistent, stage_share_id, guest_link_token FROM p_session_sharing_rooms", + ) + .get(), + ).toEqual({ + name: "Standup", + persistent: 1, + stage_share_id: "share-1", + guest_link_token: "guest-tok", + }); + expect( + db.sqlite + .prepare("SELECT COUNT(*) AS n FROM p_session_sharing_room_members") + .get(), + ).toEqual({ n: 2 }); + expect( + db.sqlite + .prepare("SELECT guest_label FROM p_session_sharing_share_participants") + .get(), + ).toEqual({ guest_label: "Guest" }); + + for (const index of [ + "idx_session_shares_session_id", + "idx_session_shares_host_id", + "idx_collab_rooms_guest_token", + "idx_collab_rooms_owner", + "idx_collab_room_members_user", + ]) { + expect(exists("index", index)).toBe(true); + } + }); + + it("follows the rename in the links between the tables", async () => { + db = await createTestDb(pluginDir, { + before: (sqlite) => { + seedUsersAndHost(sqlite); + sqlite.exec(LEGACY_DDL); + }, + }); + const ddl = (name: string) => + ( + db!.sqlite + .prepare("SELECT sql FROM sqlite_master WHERE name = ?") + .get(name) as { sql: string } + ).sql; + + expect(ddl("p_session_sharing_rooms")).toContain( + '"p_session_sharing_shares"', + ); + expect(ddl("p_session_sharing_room_members")).toContain( + '"p_session_sharing_rooms"', + ); + }); + + it("creates the tables fresh when there are no legacy ones", async () => { + db = await createTestDb(pluginDir, { + before: (sqlite) => seedUsersAndHost(sqlite), + }); + + for (const [legacy, adopted] of Object.entries(TABLES)) { + expect(exists("table", legacy)).toBe(false); + expect(exists("table", adopted)).toBe(true); + } + + db.sqlite.exec(` + INSERT INTO p_session_sharing_shares (id, host_id, owner_user_id, protocol, session_id, share_type, expires_at) + VALUES ('s', 7, 'alice', 'ssh', 'x', 'room', '2999-01-01'); + INSERT INTO p_session_sharing_rooms (id, name, owner_user_id, stage_share_id) VALUES ('r', 'R', 'alice', 's'); + INSERT INTO p_session_sharing_room_members (room_id, user_id) VALUES ('r', 'bob'); + `); + // The hand-kept set-null link: deleting the share clears the stage. + db.sqlite.exec("DELETE FROM p_session_sharing_shares WHERE id = 's'"); + expect( + db.sqlite + .prepare("SELECT stage_share_id FROM p_session_sharing_rooms") + .get(), + ).toEqual({ stage_share_id: null }); + // And deleting a user cascades to their memberships. + db.sqlite.exec("DELETE FROM users WHERE id = 'bob'"); + expect( + db.sqlite + .prepare("SELECT COUNT(*) AS n FROM p_session_sharing_room_members") + .get(), + ).toEqual({ n: 0 }); + }); +}); diff --git a/plugins/session-sharing/tests/backend/rooms.test.ts b/plugins/session-sharing/tests/backend/rooms.test.ts new file mode 100644 index 000000000..b6c5a21a2 --- /dev/null +++ b/plugins/session-sharing/tests/backend/rooms.test.ts @@ -0,0 +1,275 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { startServer, type TestServer } from "./helpers"; + +let server: TestServer | null = null; + +afterEach(async () => { + await server?.close(); + server = null; +}); + +async function roomWithBob(s: TestServer, persistent = false) { + const created = await s.request("POST", "/rooms", { + body: { name: "Standup", persistent }, + }); + const roomId = created.body.room.id as string; + await s.request("POST", `/rooms/${roomId}/members`, { + body: { userIds: ["bob"] }, + }); + return roomId; +} + +async function present(s: TestServer, roomId: string, user = "alice") { + s.live.addSession(`sess-${user}`, user); + return s.request("POST", `/rooms/${roomId}/present`, { + user, + body: { protocol: "ssh", sessionId: `sess-${user}`, hostId: 1 }, + }); +} + +describe("collab rooms", () => { + it("creates a room with the creator as host and hides the guest token", async () => { + server = await startServer(); + const res = await server.request("POST", "/rooms", { + body: { name: " Standup " }, + }); + expect(res.status).toBe(200); + expect(res.body.room).toMatchObject({ + name: "Standup", + ownerUserId: "alice", + guestLinkEnabled: false, + }); + expect(res.body.room).not.toHaveProperty("guestLinkToken"); + + const detail = await server.request("GET", `/rooms/${res.body.room.id}`); + expect(detail.body.isHost).toBe(true); + expect(detail.body.members).toEqual([ + expect.objectContaining({ userId: "alice", roomRole: "host" }), + ]); + expect(detail.body.eventsWsPath).toBe("/plugin-ws/ssh-terminal/terminal"); + expect(server.mock.audits.map((entry) => entry.action)).toContain( + "collab_room_create", + ); + }); + + it("keeps rooms private to members and invites to the host", async () => { + server = await startServer(); + const roomId = await roomWithBob(server); + + expect( + (await server.request("GET", `/rooms/${roomId}`, { user: "carol" })) + .status, + ).toBe(404); + expect( + ( + await server.request("POST", `/rooms/${roomId}/members`, { + user: "bob", + body: { userIds: ["carol"] }, + }) + ).status, + ).toBe(403); + expect( + (await server.request("GET", "/rooms", { user: "bob" })).body.rooms, + ).toHaveLength(1); + }); + + it("expands a role invite to its current members", async () => { + server = await startServer(); + server.db.sqlite.exec( + "INSERT INTO roles (id, name) VALUES (5, 'ops'); INSERT INTO user_roles (user_id, role_id) VALUES ('carol', 5)", + ); + const created = await server.request("POST", "/rooms", { + body: { name: "Ops" }, + }); + const roomId = created.body.room.id; + await server.request("POST", `/rooms/${roomId}/members`, { + body: { roleIds: [5] }, + }); + expect( + (await server.request("GET", `/rooms/${roomId}`, { user: "carol" })) + .status, + ).toBe(200); + expect( + ( + await server.request("POST", `/rooms/${roomId}/members`, { + body: { roleIds: [99] }, + }) + ).status, + ).toBe(404); + }); + + it("puts a member's live session on stage as a read-only room share", async () => { + server = await startServer(); + const roomId = await roomWithBob(server); + const res = await present(server, roomId, "bob"); + expect(res.status).toBe(200); + + const stage = await server.request("GET", `/rooms/${roomId}/stage`); + expect(stage.body.stage).toMatchObject({ + presenterUserId: "bob", + protocol: "ssh", + sessionId: "sess-bob", + }); + const row = server.db.sqlite + .prepare( + "SELECT share_type, permission_level FROM p_session_sharing_shares", + ) + .get(); + expect(row).toEqual({ share_type: "room", permission_level: "read-only" }); + }); + + it("refuses to present someone else's session", async () => { + server = await startServer(); + const roomId = await roomWithBob(server); + server.live.addSession("sess-alice", "alice"); + const res = await server.request("POST", `/rooms/${roomId}/present`, { + user: "bob", + body: { protocol: "ssh", sessionId: "sess-alice", hostId: 1 }, + }); + expect(res.status).toBe(403); + }); + + it("clears a stage whose session died", async () => { + server = await startServer(); + const roomId = await roomWithBob(server); + await present(server, roomId); + server.live.sessions.delete("sess-alice"); + + const stage = await server.request("GET", `/rooms/${roomId}/stage`, { + user: "bob", + }); + expect(stage.body.stage).toBeNull(); + const detail = await server.request("GET", `/rooms/${roomId}`); + expect(detail.body.stage.shareId).toBeNull(); + }); + + it("hands control only from the presenter or host, and only to members", async () => { + server = await startServer(); + const roomId = await roomWithBob(server); + await present(server, roomId); + + expect( + ( + await server.request("POST", `/rooms/${roomId}/control`, { + user: "bob", + body: { userId: "bob" }, + }) + ).status, + ).toBe(403); + expect( + ( + await server.request("POST", `/rooms/${roomId}/control`, { + body: { userId: "carol" }, + }) + ).status, + ).toBe(404); + + const granted = await server.request("POST", `/rooms/${roomId}/control`, { + body: { userId: "bob" }, + }); + expect(granted.body).toEqual({ controllerUserId: "bob" }); + expect(server.live.calls).toContainEqual([ + "setRoomShareControl", + "sess-alice", + expect.any(String), + "bob", + ]); + + // The controller may give it back. + expect( + ( + await server.request("POST", `/rooms/${roomId}/control`, { + user: "bob", + body: { userId: null }, + }) + ).status, + ).toBe(200); + }); + + it("rate limits hand raises", async () => { + server = await startServer(); + const roomId = await roomWithBob(server); + await present(server, roomId); + + const first = await server.request( + "POST", + `/rooms/${roomId}/control/request`, + { user: "bob" }, + ); + expect(first.body.request).toMatchObject({ + userId: "bob", + username: "bob", + }); + expect( + ( + await server.request("POST", `/rooms/${roomId}/control/request`, { + user: "bob", + }) + ).status, + ).toBe(429); + expect( + ( + await server.request("GET", `/rooms/${roomId}/control/requests`, { + user: "bob", + }) + ).status, + ).toBe(403); + expect( + (await server.request("GET", `/rooms/${roomId}/control/requests`)).body + .requests, + ).toHaveLength(1); + }); + + it("removes a member and drops them from the stage", async () => { + server = await startServer(); + const roomId = await roomWithBob(server); + await present(server, roomId); + + expect( + (await server.request("DELETE", `/rooms/${roomId}/members/alice`)).status, + ).toBe(400); + expect( + (await server.request("DELETE", `/rooms/${roomId}/members/bob`)).status, + ).toBe(200); + expect(server.live.calls).toContainEqual([ + "disconnectParticipants", + "sess-alice", + expect.any(String), + { + userId: "bob", + reason: "You were removed from the collaboration room", + }, + ]); + }); + + it("ends a one-off room for good and keeps a persistent one", async () => { + server = await startServer(); + const oneOff = await roomWithBob(server); + await present(server, oneOff); + expect( + (await server.request("POST", `/rooms/${oneOff}/end`, { user: "bob" })) + .status, + ).toBe(403); + await server.request("POST", `/rooms/${oneOff}/end`); + expect((await server.request("GET", `/rooms/${oneOff}`)).status).toBe(404); + + const persistent = await roomWithBob(server, true); + await server.request("POST", `/rooms/${persistent}/end`); + expect((await server.request("GET", `/rooms/${persistent}`)).status).toBe( + 200, + ); + }); + + it("deletes a room and its members", async () => { + server = await startServer(); + const roomId = await roomWithBob(server); + expect((await server.request("DELETE", `/rooms/${roomId}`)).status).toBe( + 200, + ); + expect( + server.db.sqlite + .prepare("SELECT COUNT(*) AS n FROM p_session_sharing_room_members") + .get(), + ).toEqual({ n: 0 }); + }); +}); diff --git a/plugins/session-sharing/tests/backend/security.test.ts b/plugins/session-sharing/tests/backend/security.test.ts new file mode 100644 index 000000000..e335fb541 --- /dev/null +++ b/plugins/session-sharing/tests/backend/security.test.ts @@ -0,0 +1,423 @@ +/** + * Guest tokens, participant permissions and expiry. These are what keep a + * share link from becoming a way into someone else's session, so each rule + * the old core code enforced has a test here. + */ + +import { afterEach, describe, expect, it } from "vitest"; +import { asUser, expireShare, startServer, type TestServer } from "./helpers"; + +let server: TestServer | null = null; + +afterEach(async () => { + await server?.close(); + server = null; +}); + +async function createShare( + s: TestServer, + body: Record = {}, +): Promise<{ shareId: string; linkToken: string | null }> { + if (!s.live.sessions.has("sess-1")) s.live.addSession("sess-1", "alice"); + const res = await s.request("POST", "/create", { + body: { + hostId: 1, + sessionId: "sess-1", + protocol: "ssh", + shareType: "link", + permissionLevel: "read-only", + ...body, + }, + }); + expect(res.status).toBe(200); + return res.body; +} + +async function roomOnStage(s: TestServer) { + const created = await s.request("POST", "/rooms", { body: { name: "R" } }); + const roomId = created.body.room.id as string; + await s.request("POST", `/rooms/${roomId}/members`, { + body: { userIds: ["bob"] }, + }); + s.live.addSession("sess-stage", "alice"); + await s.request("POST", `/rooms/${roomId}/present`, { + body: { protocol: "ssh", sessionId: "sess-stage", hostId: 1 }, + }); + const link = await s.request("POST", `/rooms/${roomId}/guest-link`, { + body: { enabled: true }, + }); + return { roomId, token: link.body.guestLinkToken as string }; +} + +describe("share link guests", () => { + it("resolves a live link without any host details", async () => { + server = await startServer(); + const { linkToken } = await createShare(server, { + permissionLevel: "read-write", + }); + + const res = await server.request("GET", `/resolve/${linkToken}`, { + user: null, + }); + expect(res.status).toBe(200); + expect(res.body).toEqual({ + protocol: "ssh", + permissionLevel: "read-write", + wsPath: `/plugin-ws/ssh-terminal/terminal?shareToken=${linkToken}`, + }); + + const joined = await server.guests.resolve({ + shareToken: linkToken!, + clientIp: "1.1.1.1", + }); + expect(joined).toEqual({ + ok: true, + share: expect.objectContaining({ + sessionId: "sess-1", + permissionLevel: "read-write", + }), + }); + }); + + it("keeps a read-only link read-only", async () => { + server = await startServer(); + const { linkToken } = await createShare(server); + const joined = await server.guests.resolve({ + shareToken: linkToken!, + clientIp: "1.1.1.1", + }); + expect(joined).toMatchObject({ share: { permissionLevel: "read-only" } }); + }); + + it("refuses an unknown or guessed token", async () => { + server = await startServer(); + await createShare(server); + for (const token of ["nope", "", "%00"]) { + const res = await server.request("GET", `/resolve/${token || "x"}`, { + user: null, + }); + expect(res.status).toBe(404); + expect( + await server.guests.resolve({ shareToken: token, clientIp: "1.1.1.1" }), + ).toMatchObject({ ok: false }); + } + }); + + it("refuses a revoked link", async () => { + server = await startServer(); + const { shareId, linkToken } = await createShare(server); + await server.request("DELETE", `/${shareId}`); + + expect( + (await server.request("GET", `/resolve/${linkToken}`, { user: null })) + .status, + ).toBe(404); + expect( + await server.guests.resolve({ + shareToken: linkToken!, + clientIp: "1.1.1.1", + }), + ).toEqual({ ok: false, reason: "Invalid or expired share link" }); + }); + + it("refuses a link once sharing is switched off", async () => { + server = await startServer(); + const { linkToken } = await createShare(server); + await server.mock.ctx.settings.set("globallyEnabled", false); + + expect( + (await server.request("GET", `/resolve/${linkToken}`, { user: null })) + .status, + ).toBe(404); + expect( + await server.guests.resolve({ + shareToken: linkToken!, + clientIp: "1.1.1.1", + }), + ).toMatchObject({ ok: false }); + }); + + it("answers 404 once the session is gone", async () => { + server = await startServer(); + const { linkToken } = await createShare(server); + server.live.sessions.delete("sess-1"); + const res = await server.request("GET", `/resolve/${linkToken}`, { + user: null, + }); + expect(res.status).toBe(404); + }); + + it("rate limits resolves per IP", async () => { + server = await startServer(); + let last = 0; + for (let i = 0; i < 31; i++) { + last = (await server.request("GET", "/resolve/guess", { user: null })) + .status; + } + expect(last).toBe(429); + }); + + it("records a guest join against the share", async () => { + server = await startServer(); + const { shareId } = await createShare(server); + await server.guests.recordJoin(shareId); + expect( + server.db.sqlite + .prepare( + "SELECT join_count, (SELECT guest_label FROM p_session_sharing_share_participants) AS label FROM p_session_sharing_shares", + ) + .get(), + ).toEqual({ join_count: 1, label: "Guest" }); + }); +}); + +describe("expired shares", () => { + it("stops resolving a link once it expires", async () => { + server = await startServer(); + const { shareId, linkToken } = await createShare(server); + expireShare(server, shareId); + + expect( + (await server.request("GET", `/resolve/${linkToken}`, { user: null })) + .status, + ).toBe(404); + expect( + await server.guests.resolve({ + shareToken: linkToken!, + clientIp: "1.1.1.1", + }), + ).toMatchObject({ ok: false }); + expect((await server.request("GET", "/host/1/active")).body.shares).toEqual( + [], + ); + }); + + it("refuses a member join on an expired user share", async () => { + server = await startServer(); + const { shareId } = await createShare(server, { + shareType: "user", + targetUserId: "bob", + }); + expireShare(server, shareId); + expect( + await asUser(server, "bob", () => server!.sharing.authorizeJoin(shareId)), + ).toBeNull(); + }); + + it("clears a room stage whose share expired and stops guests", async () => { + server = await startServer(); + const { roomId, token } = await roomOnStage(server); + const shareId = (await server.request("GET", `/rooms/${roomId}`)).body.stage + .shareId; + expireShare(server, shareId); + + const guest = await server.request("GET", `/guest/${token}`, { + user: null, + }); + expect(guest.body).toEqual({ roomName: "R", stage: null }); + expect( + await server.guests.resolve({ + roomGuestToken: token, + clientIp: "1.1.1.1", + }), + ).toEqual({ ok: false, reason: "Nothing is being presented" }); + expect( + (await server.request("GET", `/rooms/${roomId}/stage`)).body.stage, + ).toBeNull(); + }); +}); + +describe("member joins", () => { + it("lets only the target of a user share join", async () => { + server = await startServer(); + const { shareId } = await createShare(server, { + shareType: "user", + targetUserId: "bob", + permissionLevel: "read-write", + }); + + expect( + await asUser(server, "bob", () => server!.sharing.authorizeJoin(shareId)), + ).toEqual({ + share: { + id: shareId, + sessionId: "sess-1", + permissionLevel: "read-write", + }, + displayName: "bob", + }); + expect( + await asUser(server, "carol", () => + server!.sharing.authorizeJoin(shareId), + ), + ).toBeNull(); + }); + + it("refuses the target once they lose access to the host", async () => { + server = await startServer({ accessibleHostIds: [2] }); + server.live.addSession("sess-1", "alice", 1); + // Inserted directly: bob could reach host 1 when it was shared with him. + server.db.sqlite + .prepare( + "INSERT INTO p_session_sharing_shares (id, host_id, owner_user_id, protocol, session_id, share_type, target_user_id, expires_at) VALUES ('s', 1, 'alice', 'ssh', 'sess-1', 'user', 'bob', ?)", + ) + .run(new Date(Date.now() + 60_000).toISOString()); + + expect( + await asUser(server, "bob", () => server!.sharing.authorizeJoin("s")), + ).toBeNull(); + }); + + it("refuses a link share joined as a member, and no user at all", async () => { + server = await startServer(); + const { shareId } = await createShare(server); + expect( + await asUser(server, "bob", () => server!.sharing.authorizeJoin(shareId)), + ).toBeNull(); + expect(await server.sharing.authorizeJoin(shareId)).toBeNull(); + }); + + it("lets room members, and only them, join a room stage read-only", async () => { + server = await startServer(); + const { roomId } = await roomOnStage(server); + const shareId = (await server.request("GET", `/rooms/${roomId}`)).body.stage + .shareId; + + expect( + await asUser(server, "bob", () => server!.sharing.authorizeJoin(shareId)), + ).toMatchObject({ share: { permissionLevel: "read-only" } }); + expect( + await asUser(server, "carol", () => + server!.sharing.authorizeJoin(shareId), + ), + ).toBeNull(); + }); + + it("subscribes only members to room events", async () => { + server = await startServer(); + const { roomId } = await roomOnStage(server); + const socket = { readyState: 1, OPEN: 1, send: () => {} }; + expect( + await asUser(server, "carol", () => + server!.sharing.subscribeRoom(roomId, socket), + ), + ).toBe(false); + expect( + await asUser(server, "bob", () => + server!.sharing.subscribeRoom(roomId, socket), + ), + ).toBe(true); + await server.sharing.unsubscribeRoom(socket); + }); +}); + +describe("room guest links", () => { + it("resolves the stage for a guest without host details", async () => { + server = await startServer(); + const { token } = await roomOnStage(server); + + const res = await server.request("GET", `/guest/${token}`, { user: null }); + expect(res.body).toEqual({ + roomName: "R", + stage: { + protocol: "ssh", + shareId: expect.any(String), + wsPath: `/plugin-ws/ssh-terminal/terminal?roomGuestToken=${token}`, + }, + }); + expect( + await server.guests.resolve({ + roomGuestToken: token, + clientIp: "1.1.1.1", + }), + ).toMatchObject({ ok: true, share: { permissionLevel: "read-only" } }); + }); + + it("refuses the old token after a rotate and any token after disable", async () => { + server = await startServer(); + const { roomId, token } = await roomOnStage(server); + + const rotated = await server.request( + "POST", + `/rooms/${roomId}/guest-link`, + { + body: { enabled: true }, + }, + ); + expect( + (await server.request("GET", `/guest/${token}`, { user: null })).status, + ).toBe(404); + expect( + await server.guests.resolve({ + roomGuestToken: token, + clientIp: "1.1.1.1", + }), + ).toMatchObject({ ok: false }); + expect(server.live.calls).toContainEqual([ + "disconnectParticipants", + "sess-stage", + expect.any(String), + { userId: null, reason: "The guest link was rotated" }, + ]); + + await server.request("POST", `/rooms/${roomId}/guest-link`, { + body: { enabled: false }, + }); + expect( + ( + await server.request("GET", `/guest/${rotated.body.guestLinkToken}`, { + user: null, + }) + ).status, + ).toBe(404); + }); + + it("refuses a guest link after the room ends", async () => { + server = await startServer(); + const { roomId, token } = await roomOnStage(server); + await server.request("POST", `/rooms/${roomId}/end`); + expect( + (await server.request("GET", `/guest/${token}`, { user: null })).status, + ).toBe(404); + }); + + it("does not accept a room guest token as a share link, or the reverse", async () => { + server = await startServer(); + const { token } = await roomOnStage(server); + const { linkToken } = await createShare(server); + + expect( + await server.guests.resolve({ shareToken: token, clientIp: "1.1.1.1" }), + ).toMatchObject({ ok: false }); + expect( + (await server.request("GET", `/guest/${linkToken}`, { user: null })) + .status, + ).toBe(404); + }); + + it("only lets the host manage the guest link", async () => { + server = await startServer(); + const { roomId } = await roomOnStage(server); + expect( + ( + await server.request("POST", `/rooms/${roomId}/guest-link`, { + user: "bob", + body: { enabled: true }, + }) + ).status, + ).toBe(403); + }); + + it("rate limits room guests per IP", async () => { + server = await startServer(); + let refused = false; + for (let i = 0; i < 61; i++) { + const result = await server.guests.resolve({ + roomGuestToken: "guess", + clientIp: "9.9.9.9", + }); + if (!result.ok && result.reason === "Too many requests") refused = true; + } + expect(refused).toBe(true); + }); +}); diff --git a/plugins/session-sharing/tests/backend/shares.test.ts b/plugins/session-sharing/tests/backend/shares.test.ts new file mode 100644 index 000000000..693c68fd5 --- /dev/null +++ b/plugins/session-sharing/tests/backend/shares.test.ts @@ -0,0 +1,225 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { startServer, type TestServer } from "./helpers"; + +let server: TestServer | null = null; + +afterEach(async () => { + await server?.close(); + server = null; +}); + +const linkShare = (overrides: Record = {}) => ({ + hostId: 1, + sessionId: "sess-1", + protocol: "ssh", + shareType: "link", + permissionLevel: "read-only", + ...overrides, +}); + +describe("share routes", () => { + it("creates a link share for a session the caller owns", async () => { + server = await startServer(); + server.live.addSession("sess-1", "alice"); + + const res = await server.request("POST", "/create", { body: linkShare() }); + + expect(res.status).toBe(200); + expect(res.body.linkToken).toMatch(/^[A-Za-z0-9_-]{32}$/); + const active = await server.request("GET", "/host/1/active"); + expect(active.body.shares).toHaveLength(1); + expect(active.body.shares[0]).toMatchObject({ + ownerUserId: "alice", + shareType: "link", + permissionLevel: "read-only", + }); + }); + + it("refuses a session someone else owns", async () => { + server = await startServer(); + server.live.addSession("sess-1", "bob"); + + const res = await server.request("POST", "/create", { body: linkShare() }); + expect(res.status).toBe(403); + }); + + it("refuses when sharing is off for the host or everywhere", async () => { + server = await startServer({ settings: { globallyEnabled: false } }); + server.live.addSession("sess-1", "alice"); + expect( + (await server.request("POST", "/create", { body: linkShare() })).status, + ).toBe(403); + await server.close(); + + server = await startServer(); + server.live.addSession("sess-1", "alice"); + await server.mock.ctx.settings.setHost(1, "allowSessionSharing", false); + expect( + (await server.request("POST", "/create", { body: linkShare() })).status, + ).toBe(403); + }); + + it("validates the request", async () => { + server = await startServer(); + for (const body of [ + {}, + linkShare({ protocol: "ftp" }), + linkShare({ shareType: "room" }), + linkShare({ permissionLevel: "owner" }), + linkShare({ shareType: "user" }), + ]) { + expect((await server.request("POST", "/create", { body })).status).toBe( + 400, + ); + } + }); + + it("shares with a user only when they can reach the host", async () => { + server = await startServer({ accessibleHostIds: [1] }); + server.live.addSession("sess-1", "alice", 1); + server.live.addSession("sess-2", "alice", 2); + + const ok = await server.request("POST", "/create", { + body: linkShare({ shareType: "user", targetUserId: "bob" }), + }); + expect(ok.status).toBe(200); + expect(ok.body.linkToken).toBeNull(); + + const noAccess = await server.request("POST", "/create", { + body: linkShare({ + hostId: 2, + sessionId: "sess-2", + shareType: "user", + targetUserId: "bob", + }), + }); + expect(noAccess.status).toBe(403); + + const unknownUser = await server.request("POST", "/create", { + body: linkShare({ shareType: "user", targetUserId: "nobody" }), + }); + expect(unknownUser.status).toBe(403); + }); + + it("lets the owner or an admin revoke, and kicks SSH guests", async () => { + server = await startServer({ admins: ["carol"] }); + server.live.addSession("sess-1", "alice"); + const { body } = await server.request("POST", "/create", { + body: linkShare(), + }); + + expect( + (await server.request("DELETE", `/${body.shareId}`, { user: "bob" })) + .status, + ).toBe(403); + expect( + (await server.request("DELETE", `/${body.shareId}`, { user: "carol" })) + .status, + ).toBe(200); + expect(server.live.calls[0]).toEqual([ + "ownerEndSession", + "sess-1", + "Session share revoked by owner", + ]); + expect( + ( + await server.request("GET", `/resolve/${body.linkToken}`, { + user: null, + }) + ).status, + ).toBe(404); + expect((await server.request("DELETE", "/missing")).status).toBe(404); + }); + + it("ends a shared session for its owner only", async () => { + server = await startServer(); + server.live.addSession("sess-1", "alice"); + const { body } = await server.request("POST", "/create", { + body: linkShare(), + }); + + expect( + (await server.request("POST", `/${body.shareId}/end`, { user: "bob" })) + .status, + ).toBe(403); + expect((await server.request("POST", `/${body.shareId}/end`)).status).toBe( + 200, + ); + expect(server.live.calls).toContainEqual([ + "ownerEndSession", + "sess-1", + "Session ended by owner", + ]); + }); + + it("lists live sessions shared with the caller", async () => { + server = await startServer(); + server.live.addSession("sess-1", "alice"); + const { body } = await server.request("POST", "/create", { + body: linkShare({ shareType: "user", targetUserId: "bob" }), + }); + + const res = await server.request("GET", "/shared-with-me", { user: "bob" }); + expect(res.body).toEqual([ + expect.objectContaining({ + sessionId: "sess-1", + hostName: "host-1", + isOwnSession: false, + sharedByUsername: "alice", + shareId: body.shareId, + permissionLevel: "read-only", + }), + ]); + + server.live.sessions.delete("sess-1"); + expect( + (await server.request("GET", "/shared-with-me", { user: "bob" })).body, + ).toEqual([]); + }); + + it("lists users and roles for the pickers", async () => { + server = await startServer(); + server.db.sqlite + .prepare("INSERT INTO roles (id, name, display_name) VALUES (?, ?, ?)") + .run(3, "ops", "Operations"); + const res = await server.request("GET", "/directory"); + expect(res.body.users.map((u: { id: string }) => u.id)).toEqual([ + "alice", + "bob", + "carol", + ]); + expect(res.body.roles).toEqual([ + { id: 3, name: "ops", displayName: "Operations" }, + ]); + }); + + it("denies every signed-in route without the use permission", async () => { + server = await startServer({ permissions: [] }); + for (const [method, path] of [ + ["POST", "/create"], + ["GET", "/host/1/active"], + ["GET", "/shared-with-me"], + ["GET", "/directory"], + ["DELETE", "/share-1"], + ["POST", "/share-1/end"], + ["POST", "/rooms"], + ["GET", "/rooms"], + ["GET", "/rooms/r"], + ["POST", "/rooms/r/members"], + ["DELETE", "/rooms/r/members/bob"], + ["POST", "/rooms/r/present"], + ["POST", "/rooms/r/stop"], + ["GET", "/rooms/r/stage"], + ["POST", "/rooms/r/control"], + ["POST", "/rooms/r/control/request"], + ["GET", "/rooms/r/control/requests"], + ["DELETE", "/rooms/r/control/requests/bob"], + ["POST", "/rooms/r/guest-link"], + ["POST", "/rooms/r/end"], + ["DELETE", "/rooms/r"], + ]) { + const res = await server.request(method, path, { body: {} }); + expect([method, path, res.status]).toEqual([method, path, 403]); + } + }); +}); diff --git a/plugins/session-sharing/tests/frontend/SharedSessionView.test.tsx b/plugins/session-sharing/tests/frontend/SharedSessionView.test.tsx new file mode 100644 index 000000000..57c469093 --- /dev/null +++ b/plugins/session-sharing/tests/frontend/SharedSessionView.test.tsx @@ -0,0 +1,113 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import type { ComponentType } from "react"; +import type { PluginApiClient } from "@termix/plugin-sdk/frontend"; +import { + renderWithApp, + type RenderedPluginApp, +} from "@termix/plugin-sdk/testing"; +import { PluginComponent } from "@termix/plugin-sdk/ui"; +import type { PluginManifest } from "@termix/plugin-sdk/manifest"; +import * as plugin from "../../src/frontend/index"; +import SharedSessionView from "../../src/frontend/SharedSessionView"; +import manifestJson from "../../manifest.json"; +import locales from "../../locales/en.json"; + +vi.mock("react-xtermjs", () => ({ + useXTerm: () => ({ + instance: { + options: {}, + loadAddon: vi.fn(), + open: vi.fn(), + write: vi.fn(), + onData: vi.fn(), + }, + ref: { current: document.createElement("div") }, + }), +})); + +vi.mock("@xterm/addon-fit", () => ({ + FitAddon: class FitAddon { + fit() {} + }, +})); + +const manifest = manifestJson as unknown as PluginManifest; +const guestView = locales.sessionSharing.guestView; + +let rendered: RenderedPluginApp | null = null; + +afterEach(async () => { + await rendered?.deactivate(); + rendered = null; +}); + +function httpError(status: number) { + return Object.assign(new Error(`HTTP ${status}`), { response: { status } }); +} + +/** The shared-link page, rendered as a guest inside the plugin's scope. */ +async function renderShared(search: string, get: PluginApiClient["get"]) { + window.history.pushState({}, "", `/?${search}`); + rendered = await renderWithApp(plugin, { + manifest, + locales, + guest: true, + api: { get } as unknown as PluginApiClient, + }); + rendered.app.registerComponent( + "session-sharing.testSharedView", + SharedSessionView as ComponentType>, + ); + rendered.app.registerSlotContribution("session.remoteDisplay", { + actionId: "fixture.display", + titleKey: "nav.collab", + kind: "component", + component: () =>
, + }); + render(); +} + +describe("SharedSessionView", () => { + it("says the link is invalid when there is no token", async () => { + const get = vi.fn(); + await renderShared("view=shared", get as never); + expect(await screen.findByText(guestView.linkInvalid)).toBeTruthy(); + expect(get).not.toHaveBeenCalled(); + }); + + it("says the link is invalid for a revoked or expired link", async () => { + await renderShared( + "view=shared&token=gone", + vi.fn(async () => { + throw httpError(404); + }) as never, + ); + expect(await screen.findByText(guestView.linkInvalid)).toBeTruthy(); + }); + + it("asks the guest to wait when rate limited", async () => { + await renderShared( + "view=shared&token=busy", + vi.fn(async () => { + throw httpError(429); + }) as never, + ); + expect(await screen.findByText(guestView.rateLimited)).toBeTruthy(); + }); + + it("draws a remote desktop share through the display slot", async () => { + const get = vi.fn(async () => ({ + data: { + protocol: "rdp", + permissionLevel: "read-only", + wsPath: "/plugin-ws/remote-desktop/display", + connectParams: { token: "t" }, + }, + })); + await renderShared("view=shared&token=ok", get as never); + expect(await screen.findByTestId("remote-display")).toBeTruthy(); + expect(screen.getByText(guestView.readOnlyBadge)).toBeTruthy(); + expect(get).toHaveBeenCalledWith("/resolve/ok"); + }); +}); diff --git a/plugins/session-sharing/tests/frontend/activate.test.tsx b/plugins/session-sharing/tests/frontend/activate.test.tsx new file mode 100644 index 000000000..948fc3a46 --- /dev/null +++ b/plugins/session-sharing/tests/frontend/activate.test.tsx @@ -0,0 +1,196 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { act, fireEvent, screen, waitFor } from "@testing-library/react"; +import { + invokeAction, + type PluginApiClient, +} from "@termix/plugin-sdk/frontend"; +import { + renderWithApp, + type RenderedPluginApp, +} from "@termix/plugin-sdk/testing"; +import type { PluginManifest } from "@termix/plugin-sdk/manifest"; +import * as plugin from "../../src/frontend/index"; +import manifestJson from "../../manifest.json"; +import locales from "../../locales/en.json"; + +const manifest = manifestJson as unknown as PluginManifest; + +let rendered: RenderedPluginApp | null = null; + +afterEach(async () => { + await rendered?.deactivate(); + rendered = null; +}); + +function stubApi(overrides: Partial = {}): PluginApiClient { + return { + get: vi.fn(async (url: string) => ({ + data: + url === "/directory" + ? { users: [{ id: "bob", username: "bob" }], roles: [] } + : url.includes("/active") + ? { shares: [] } + : url === "/rooms" + ? { rooms: [] } + : {}, + })), + post: vi.fn(async () => ({ + data: { shareId: "s1", linkToken: "tok", expiresAt: "2999-01-01" }, + })), + put: vi.fn(async () => ({ data: {} })), + patch: vi.fn(async () => ({ data: {} })), + delete: vi.fn(async () => ({ data: {} })), + ...overrides, + } as PluginApiClient; +} + +describe("session-sharing activate", () => { + it("registers the rooms rail item, panel and tab, and the share buttons", async () => { + rendered = await renderWithApp(plugin, { + manifest, + locales, + api: stubApi(), + }); + + expect(rendered.registered.railItems()).toEqual([ + expect.objectContaining({ + id: "collab", + permission: "session-sharing.use", + }), + ]); + expect(rendered.registered.panels()).toEqual(["collab"]); + expect(rendered.registered.tabs()).toEqual(["collab"]); + expect(rendered.registered.slot("terminal.toolbar")).toEqual([ + "session-sharing.share", + ]); + expect(rendered.registered.slot("remote-desktop.toolbar")).toEqual([ + "session-sharing.share", + ]); + expect(rendered.registered.slot("shell.overlay")).toHaveLength(2); + expect(rendered.registered.actions().sort()).toEqual([ + "session-sharing.share", + "sessions.sharedWithMe", + ]); + }); + + it("registers only the guest views on a guest page", async () => { + rendered = await renderWithApp(plugin, { + manifest, + locales, + api: stubApi(), + guest: true, + }); + expect(rendered.registered.tabs()).toEqual(["collab"]); + expect(rendered.registered.railItems()).toEqual([]); + expect(rendered.registered.actions()).toEqual([]); + }); + + it("opens the share dialog for a terminal's session", async () => { + const api = stubApi(); + rendered = await renderWithApp(plugin, { manifest, locales, api }); + rendered.renderSlot("shell.overlay"); + + await act(() => + invokeAction("session-sharing.share", { + getShareTarget: () => ({ + hostId: 4, + sessionId: "sess-9", + protocol: "ssh", + }), + }), + ); + + expect( + await screen.findByText(locales.sessionSharing.modalTitle), + ).toBeTruthy(); + await waitFor(() => expect(api.get).toHaveBeenCalledWith("/host/4/active")); + }); + + it("opens the dialog for a remote desktop session once it is up", async () => { + rendered = await renderWithApp(plugin, { + manifest, + locales, + api: stubApi(), + }); + rendered.renderSlot("shell.overlay"); + + await act(() => + invokeAction("session-sharing.share", { + hostId: 2, + sessionId: null, + protocol: "rdp", + }), + ); + expect(screen.queryByText(locales.sessionSharing.modalTitle)).toBeNull(); + + await act(() => + invokeAction("session-sharing.share", { + hostId: 2, + sessionId: "guac-1", + protocol: "rdp", + }), + ); + expect( + await screen.findByText(locales.sessionSharing.modalTitle), + ).toBeTruthy(); + }); + + it("creates a link share from the dialog", async () => { + const api = stubApi(); + rendered = await renderWithApp(plugin, { manifest, locales, api }); + rendered.renderSlot("shell.overlay"); + await act(() => + invokeAction("session-sharing.share", { + getShareTarget: () => ({ + hostId: 1, + sessionId: "sess-1", + protocol: "ssh", + }), + }), + ); + + fireEvent.click( + await screen.findByRole("button", { + name: locales.sessionSharing.createLinkButton, + }), + ); + await waitFor(() => + expect(api.post).toHaveBeenCalledWith( + "/create", + expect.objectContaining({ + hostId: 1, + sessionId: "sess-1", + shareType: "link", + permissionLevel: "read-only", + }), + ), + ); + }); + + it("answers sessions shared with the user through the api", async () => { + const shared = [{ sessionId: "s", isOwnSession: false }]; + rendered = await renderWithApp(plugin, { + manifest, + locales, + api: stubApi({ + get: vi.fn(async () => ({ data: shared })) as never, + }), + }); + await expect(invokeAction("sessions.sharedWithMe")).resolves.toEqual( + shared, + ); + }); + + it("removes everything it registered on deactivate", async () => { + const app = await renderWithApp(plugin, { + manifest, + locales, + api: stubApi(), + }); + await app.deactivate(); + expect(app.registered.tabs()).toEqual([]); + expect(app.registered.panels()).toEqual([]); + expect(app.registered.slot("terminal.toolbar")).toEqual([]); + expect(app.registered.actions()).toEqual([]); + }); +}); diff --git a/plugins/session-sharing/tsconfig.json b/plugins/session-sharing/tsconfig.json new file mode 100644 index 000000000..7b35314c3 --- /dev/null +++ b/plugins/session-sharing/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@termix/plugin-sdk/tsconfig.plugin.json", + "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"] +} diff --git a/plugins/session-sharing/vitest.config.ts b/plugins/session-sharing/vitest.config.ts new file mode 100644 index 000000000..31ba004fc --- /dev/null +++ b/plugins/session-sharing/vitest.config.ts @@ -0,0 +1,3 @@ +import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset"; + +export default pluginVitestConfig(import.meta.url); diff --git a/plugins/ssh-terminal/manifest.json b/plugins/ssh-terminal/manifest.json index 51c16489e..b52304c2c 100644 --- a/plugins/ssh-terminal/manifest.json +++ b/plugins/ssh-terminal/manifest.json @@ -27,7 +27,8 @@ { "service": "sessions.live", "version": "1.0.0", - "permission": "ssh-terminal.sessions" + "permission": "ssh-terminal.sessions", + "names": ["ssh"] }, { "service": "terminal.history", diff --git a/plugins/ssh-terminal/src/backend/index.ts b/plugins/ssh-terminal/src/backend/index.ts index 7ce0b9def..0afd20a9c 100644 --- a/plugins/ssh-terminal/src/backend/index.ts +++ b/plugins/ssh-terminal/src/backend/index.ts @@ -153,7 +153,8 @@ export async function activate(ctx: PluginContext) { }, idleTimeoutMinutes: () => timeoutMinutes, }; - ctx.services.provide("sessions.live", liveSessions); + // sessions.live is keyed by session type; remote desktop provides the others. + ctx.services.provide("sessions.live", liveSessions, { name: "ssh" }); const terminalHistory: TerminalHistoryV1 = { list: async (hostId, limit = 200) => { diff --git a/plugins/ssh-terminal/src/backend/services.ts b/plugins/ssh-terminal/src/backend/services.ts index 318feb051..16b82c94b 100644 --- a/plugins/ssh-terminal/src/backend/services.ts +++ b/plugins/ssh-terminal/src/backend/services.ts @@ -1,11 +1,13 @@ /** * The services this plugin provides and consumes. * - * Provided: "sessions.live" (live SSH sessions, for session sharing, collab - * and recording) and "terminal.history" (command history, for the AI - * assistant). Consumed, all optional: "tmux.sessions" (B10), - * "sessions.sharing" (B12) and "recordings.writer" (B13). The terminal keeps - * working without any of them: no tmux attach, no shared joins, no recording. + * Provided: "sessions.live" as its "ssh" provider (live SSH sessions, for + * session sharing, collab rooms and recording; remote desktop provides the + * other session types) and "terminal.history" (command history, for the AI + * assistant). Consumed, all optional: "tmux.sessions" (tmux-monitor), + * "sessions.sharing" (session-sharing) and "recordings.writer" (B13). The + * terminal keeps working without any of them: no tmux attach, no shared + * joins, no recording. */ export interface LiveSessionInfo { diff --git a/plugins/ssh-terminal/src/frontend/terminal/Terminal.tsx b/plugins/ssh-terminal/src/frontend/terminal/Terminal.tsx index 71e3446a7..43fe15264 100644 --- a/plugins/ssh-terminal/src/frontend/terminal/Terminal.tsx +++ b/plugins/ssh-terminal/src/frontend/terminal/Terminal.tsx @@ -784,8 +784,30 @@ const TerminalInner = forwardRef( openSidePanel: (panelId, props) => setDock({ id: panelId, props: props ?? {} }), runCommand: handleRunCommandInTerminal, + getShareTarget: () => + isConnected && + !isQuickConnect && + !hostConfig.joinShareId && + typeof hostConfig.id === "number" && + sessionIdRef.current + ? { + hostId: hostConfig.id, + sessionId: sessionIdRef.current, + protocol: "ssh" as const, + tabInstanceId: hostConfig.instanceId, + } + : null, }), - [host, terminal, handleRunCommandInTerminal], + [ + host, + terminal, + handleRunCommandInTerminal, + isConnected, + isQuickConnect, + hostConfig.joinShareId, + hostConfig.id, + hostConfig.instanceId, + ], ); const overlayProps = useMemo( @@ -1423,27 +1445,8 @@ const TerminalInner = forwardRef( onOpenFileManager?.("/"); } }, - getShareTarget: () => - isConnected && - !isQuickConnect && - !hostConfig.joinShareId && - typeof hostConfig.id === "number" && - sessionIdRef.current - ? { - hostId: hostConfig.id, - sessionId: sessionIdRef.current, - protocol: "ssh" as const, - tabInstanceId: hostConfig.instanceId, - } - : null, }), - [ - isConnected, - terminal, - isQuickConnect, - hostConfig.joinShareId, - hostConfig.id, - ], + [isConnected, terminal], ); function getCopyOnSelect() { diff --git a/plugins/ssh-terminal/src/frontend/terminal/terminal-slots.ts b/plugins/ssh-terminal/src/frontend/terminal/terminal-slots.ts index 6deeecc03..13dbb28bc 100644 --- a/plugins/ssh-terminal/src/frontend/terminal/terminal-slots.ts +++ b/plugins/ssh-terminal/src/frontend/terminal/terminal-slots.ts @@ -29,6 +29,16 @@ export interface TerminalSlotApi { openSidePanel: (panelId: string, props?: Record) => void; /** Types a command into the session and runs it. */ runCommand: (command: string) => void; + /** + * What sharing this session needs, or null while there is nothing to share + * (not connected yet, a quick connect, or a session joined from a share). + */ + getShareTarget: () => { + hostId: number; + sessionId: string; + protocol: "ssh"; + tabInstanceId?: string; + } | null; } export interface TerminalToolbarStatusProps { diff --git a/plugins/ssh-terminal/src/frontend/terminal/terminal-types.ts b/plugins/ssh-terminal/src/frontend/terminal/terminal-types.ts index 44d601195..da48e7cbe 100644 --- a/plugins/ssh-terminal/src/frontend/terminal/terminal-types.ts +++ b/plugins/ssh-terminal/src/frontend/terminal/terminal-types.ts @@ -33,11 +33,4 @@ export interface TerminalHandle { notifyResize: () => void; refresh: () => void; getApplicationCursorKeysMode: () => boolean; - /** What the shell's share action needs, or null while there is nothing to share. */ - getShareTarget: () => { - hostId: number; - sessionId: string; - protocol: "ssh"; - tabInstanceId?: string; - } | null; } diff --git a/plugins/ssh-terminal/tests/backend/activate.test.ts b/plugins/ssh-terminal/tests/backend/activate.test.ts index 7b7abe143..41ae8c553 100644 --- a/plugins/ssh-terminal/tests/backend/activate.test.ts +++ b/plugins/ssh-terminal/tests/backend/activate.test.ts @@ -30,9 +30,11 @@ describe("ssh-terminal activate", () => { expect(server.mock.wsRoutes).toEqual([{ path: "/terminal", raw: false }]); }); - it("provides sessions.live and terminal.history", async () => { + it("provides sessions.live as ssh and terminal.history", async () => { server = await startServer(); - const live = server.mock.services.get("sessions.live") as LiveSessionsV1; + const live = server.mock.services.get( + "sessions.live#ssh", + ) as LiveSessionsV1; expect(live.getSession("missing")).toBeNull(); expect(live.listForUser("user-1")).toEqual([]); expect(live.write("missing", "ls\r")).toBe(false); @@ -52,7 +54,9 @@ describe("ssh-terminal activate", () => { it("keeps a plugin caller to its own actor's sessions", async () => { server = await startServer(); - const live = server.mock.services.get("sessions.live") as LiveSessionsV1; + const live = server.mock.services.get( + "sessions.live#ssh", + ) as LiveSessionsV1; const other = await server.mock.ctx.asUser("user-2", async () => live.listForUser("user-1"), ); @@ -61,7 +65,9 @@ describe("ssh-terminal activate", () => { it("uses the admin session timeout for detached sessions", async () => { server = await startServer({ settings: { sessionTimeoutMinutes: 90 } }); - const live = server.mock.services.get("sessions.live") as LiveSessionsV1; + const live = server.mock.services.get( + "sessions.live#ssh", + ) as LiveSessionsV1; expect(live.idleTimeoutMinutes()).toBe(90); }); diff --git a/plugins/ssh-terminal/tests/frontend/terminal/TerminalToolbar.test.tsx b/plugins/ssh-terminal/tests/frontend/terminal/TerminalToolbar.test.tsx index 3df6fdbe1..760657bdb 100644 --- a/plugins/ssh-terminal/tests/frontend/terminal/TerminalToolbar.test.tsx +++ b/plugins/ssh-terminal/tests/frontend/terminal/TerminalToolbar.test.tsx @@ -901,6 +901,7 @@ describe("TerminalToolbar Phase 1", () => { sessionId: () => null, openSidePanel: vi.fn(), runCommand: vi.fn(), + getShareTarget: () => null, }; renderToolbar({ slotApi }); await userEvent.click( diff --git a/scripts/shell-plugin-id-allowlist.json b/scripts/shell-plugin-id-allowlist.json index ca5c29612..f5674441c 100644 --- a/scripts/shell-plugin-id-allowlist.json +++ b/scripts/shell-plugin-id-allowlist.json @@ -1,22 +1,10 @@ { "$comment": "Literals src/ui may still spell that name a plugin id or a plugin-owned view. Each is data, not knowledge of a plugin. Checked by scripts/check-shell-plugin-ids.cjs; D1 and Phase B empty it.", "files": { - "src/ui/api/collab-api.ts": { - "literals": ["rdp", "telnet", "vnc"], - "reason": "Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableTelnet, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it." - }, "src/ui/api/session-log-api.ts": { "literals": ["rdp", "telnet", "vnc"], "reason": "Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableTelnet, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it." }, - "src/ui/api/session-sharing-api.ts": { - "literals": ["rdp", "telnet", "vnc"], - "reason": "Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableTelnet, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it." - }, - "src/ui/features/collab/CollabRoomTab.tsx": { - "literals": ["rdp", "telnet", "vnc"], - "reason": "Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableTelnet, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it." - }, "src/ui/lib/connection-origin.ts": { "literals": ["rdp", "telnet", "vnc"], "reason": "Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableTelnet, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it." diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index 749f84ce3..bc51f56ea 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -11,8 +11,6 @@ import alertRoutes from "./routes/alerts.js"; import credentialsRoutes from "./routes/credentials.js"; import sshAuthRoutes from "./routes/ssh-auth-routes.js"; import sessionLogRoutes from "./routes/session-log-routes.js"; -import sessionSharingRoutes from "../hosts/session-sharing/routes.js"; -import collabRoutes from "../hosts/collab/routes.js"; import rbacRoutes from "./routes/rbac.js"; import openTabsRoutes from "./routes/open-tabs.js"; import userPreferencesRoutes from "./routes/user-preferences.js"; @@ -1808,8 +1806,6 @@ app.use("/alerts", alertRoutes); app.use("/credentials", credentialsRoutes); app.use("/ssh-auth", sshAuthRoutes); app.use("/session_logs", sessionLogRoutes); -app.use("/session-sharing", sessionSharingRoutes); -app.use("/collab", collabRoutes); app.use("/rbac", rbacRoutes); app.use("/open-tabs", openTabsRoutes); app.use("/user-preferences", userPreferencesRoutes); diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index 1924ab539..402edde02 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -500,69 +500,6 @@ async function initializeCompleteDatabase(): Promise { FOREIGN KEY (access_id) REFERENCES host_access (id) ON DELETE SET NULL ); - CREATE TABLE IF NOT EXISTS session_shares ( - id TEXT PRIMARY KEY, - host_id INTEGER NOT NULL, - owner_user_id TEXT NOT NULL, - protocol TEXT NOT NULL, - session_id TEXT NOT NULL, - tab_instance_id TEXT, - share_type TEXT NOT NULL, - target_user_id TEXT, - link_token TEXT UNIQUE, - permission_level TEXT NOT NULL DEFAULT 'read-only', - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - expires_at TEXT NOT NULL, - revoked_at TEXT, - last_joined_at TEXT, - join_count INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE, - FOREIGN KEY (owner_user_id) REFERENCES users (id) ON DELETE CASCADE, - FOREIGN KEY (target_user_id) REFERENCES users (id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS session_share_participants ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - share_id TEXT NOT NULL, - user_id TEXT, - guest_label TEXT, - joined_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - left_at TEXT, - FOREIGN KEY (share_id) REFERENCES session_shares (id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS collab_rooms ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - owner_user_id TEXT NOT NULL, - persistent INTEGER NOT NULL DEFAULT 0, - presenter_user_id TEXT, - stage_protocol TEXT, - stage_host_id INTEGER, - stage_share_id TEXT, - guest_link_token TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - ended_at TEXT, - FOREIGN KEY (owner_user_id) REFERENCES users (id) ON DELETE CASCADE, - FOREIGN KEY (presenter_user_id) REFERENCES users (id) ON DELETE SET NULL, - FOREIGN KEY (stage_host_id) REFERENCES ssh_data (id) ON DELETE SET NULL, - FOREIGN KEY (stage_share_id) REFERENCES session_shares (id) ON DELETE SET NULL - ); - - CREATE TABLE IF NOT EXISTS collab_room_members ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - room_id TEXT NOT NULL, - user_id TEXT NOT NULL, - room_role TEXT NOT NULL DEFAULT 'member', - added_by TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE (room_id, user_id), - FOREIGN KEY (room_id) REFERENCES collab_rooms (id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, - FOREIGN KEY (added_by) REFERENCES users (id) ON DELETE SET NULL - ); - CREATE TABLE IF NOT EXISTS secret_sources ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, @@ -2060,10 +1997,6 @@ const migrateSchema = () => { } addColumnIfNotExists("users", "sso_provider_id", "INTEGER"); - addColumnIfNotExists("collab_rooms", "guest_link_token", "TEXT"); - sqlite.exec( - "CREATE UNIQUE INDEX IF NOT EXISTS idx_collab_rooms_guest_token ON collab_rooms (guest_link_token)", - ); try { const usersTableInfo = sqlite.prepare("PRAGMA table_info(users)").all() as Array<{ diff --git a/src/backend/database/db/performance-indexes.ts b/src/backend/database/db/performance-indexes.ts index 46f121bd3..2f6f7d502 100644 --- a/src/backend/database/db/performance-indexes.ts +++ b/src/backend/database/db/performance-indexes.ts @@ -135,16 +135,6 @@ export const PERFORMANCE_INDEXES: PerformanceIndex[] = [ table: "session_recordings", columns: "host_id", }, - { - name: "idx_session_shares_session_id", - table: "session_shares", - columns: "session_id", - }, - { - name: "idx_session_shares_host_id", - table: "session_shares", - columns: "host_id", - }, // Automations. { diff --git a/src/backend/database/db/schema.mysql.ts b/src/backend/database/db/schema.mysql.ts index b8ca803d2..996a57c1e 100644 --- a/src/backend/database/db/schema.mysql.ts +++ b/src/backend/database/db/schema.mysql.ts @@ -761,70 +761,6 @@ export const sessionRecordings = mysqlTable( ], ); -export const sessionShares = mysqlTable( - "session_shares", - { - id: varchar("id", { length: 255 }).primaryKey(), - - hostId: int("host_id") - .notNull() - .references(() => hosts.id, { onDelete: "cascade" }), - ownerUserId: varchar("owner_user_id", { length: 255 }) - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - - protocol: varchar("protocol", { length: 255 }).notNull(), - - // Live-session binding: TerminalSessionManager's session.id for SSH, or - // guacd's own guacamoleConnectionId for rdp/vnc/telnet. Neither is a DB - // row (process-local, in-memory) so this intentionally has no FK. - sessionId: varchar("session_id", { length: 255 }).notNull(), - tabInstanceId: text("tab_instance_id"), - - shareType: text("share_type").notNull(), // "link" | "user" - targetUserId: varchar("target_user_id", { length: 255 }).references(() => users.id, { - onDelete: "cascade", - }), - linkToken: varchar("link_token", { length: 255 }).unique(), - - permissionLevel: text("permission_level").notNull().default("read-only"), - - createdAt: varchar("created_at", { length: 255 }) - .notNull() - .default(sql`(CURRENT_TIMESTAMP)`), - expiresAt: varchar("expires_at", { length: 255 }).notNull(), - revokedAt: text("revoked_at"), - - lastJoinedAt: text("last_joined_at"), - joinCount: int("join_count").notNull().default(0), - }, - // Resolved from the live session on join, and listed per host. - (table) => [ - index("idx_session_shares_session_id").on(table.sessionId), - index("idx_session_shares_host_id").on(table.hostId), - ], -); - -export const sessionShareParticipants = mysqlTable( - "session_share_participants", - { - id: int("id").autoincrement().primaryKey(), - shareId: varchar("share_id", { length: 255 }) - .notNull() - .references(() => sessionShares.id, { onDelete: "cascade" }), - - userId: varchar("user_id", { length: 255 }).references(() => users.id, { - onDelete: "cascade", - }), - guestLabel: text("guest_label"), - - joinedAt: text("joined_at") - .notNull() - .default(sql`(CURRENT_TIMESTAMP)`), - leftAt: text("left_at"), - }, -); - export const opksshTokens = mysqlTable( "opkssh_tokens", { @@ -1569,81 +1505,6 @@ export const aiProposals = mysqlTable( // --- collab rooms --- -/** - * A collaboration room: a group of users watching one "stage" - the live - * session the current presenter is showing. The stage points at a - * shareType="room" row in session_shares, so transport, gating, recording and - * expiry all reuse the session-sharing machinery. - */ -export const collabRooms = mysqlTable( - "collab_rooms", - { - id: varchar("id", { length: 255 }).primaryKey(), - name: varchar("name", { length: 255 }).notNull(), - ownerUserId: varchar("owner_user_id", { length: 255 }) - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - - // Persistent rooms survive being emptied and can be re-used; one-off - // rooms are ended explicitly and never listed again. - persistent: boolean("persistent") - .notNull() - .default(false), - - presenterUserId: varchar("presenter_user_id", { length: 255 }).references(() => users.id, { - onDelete: "set null", - }), - stageProtocol: text("stage_protocol"), - stageHostId: int("stage_host_id").references(() => hosts.id, { - onDelete: "set null", - }), - stageShareId: varchar("stage_share_id", { length: 255 }).references(() => sessionShares.id, { - onDelete: "set null", - }), - - // Set = anonymous guests may watch the stage through this token. - guestLinkToken: varchar("guest_link_token", { length: 255 }), - - createdAt: varchar("created_at", { length: 255 }) - .notNull() - .default(sql`(CURRENT_TIMESTAMP)`), - endedAt: text("ended_at"), - }, - (table) => [ - index("idx_collab_rooms_owner").on(table.ownerUserId), - uniqueIndex("idx_collab_rooms_guest_token").on(table.guestLinkToken), - ], -); - -export const collabRoomMembers = mysqlTable( - "collab_room_members", - { - id: int("id").autoincrement().primaryKey(), - roomId: varchar("room_id", { length: 255 }) - .notNull() - .references(() => collabRooms.id, { onDelete: "cascade" }), - userId: varchar("user_id", { length: 255 }) - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - - // "host" runs the room: invites, force-switches the presenter, ends it. - roomRole: text("room_role").notNull().default("member"), - addedBy: varchar("added_by", { length: 255 }).references(() => users.id, { - onDelete: "set null", - }), - createdAt: varchar("created_at", { length: 255 }) - .notNull() - .default(sql`(CURRENT_TIMESTAMP)`), - }, - (table) => [ - uniqueIndex("idx_collab_room_members_room_user").on( - table.roomId, - table.userId, - ), - index("idx_collab_room_members_user").on(table.userId), - ], -); - // --- secret sources --- /** diff --git a/src/backend/database/db/schema.pg.ts b/src/backend/database/db/schema.pg.ts index 63420e33a..8bd1497d4 100644 --- a/src/backend/database/db/schema.pg.ts +++ b/src/backend/database/db/schema.pg.ts @@ -762,70 +762,6 @@ export const sessionRecordings = pgTable( ], ); -export const sessionShares = pgTable( - "session_shares", - { - id: varchar("id", { length: 255 }).primaryKey(), - - hostId: integer("host_id") - .notNull() - .references(() => hosts.id, { onDelete: "cascade" }), - ownerUserId: varchar("owner_user_id", { length: 255 }) - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - - protocol: varchar("protocol", { length: 255 }).notNull(), - - // Live-session binding: TerminalSessionManager's session.id for SSH, or - // guacd's own guacamoleConnectionId for rdp/vnc/telnet. Neither is a DB - // row (process-local, in-memory) so this intentionally has no FK. - sessionId: varchar("session_id", { length: 255 }).notNull(), - tabInstanceId: text("tab_instance_id"), - - shareType: text("share_type").notNull(), // "link" | "user" - targetUserId: varchar("target_user_id", { length: 255 }).references(() => users.id, { - onDelete: "cascade", - }), - linkToken: varchar("link_token", { length: 255 }).unique(), - - permissionLevel: text("permission_level").notNull().default("read-only"), - - createdAt: varchar("created_at", { length: 255 }) - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - expiresAt: varchar("expires_at", { length: 255 }).notNull(), - revokedAt: text("revoked_at"), - - lastJoinedAt: text("last_joined_at"), - joinCount: integer("join_count").notNull().default(0), - }, - // Resolved from the live session on join, and listed per host. - (table) => [ - index("idx_session_shares_session_id").on(table.sessionId), - index("idx_session_shares_host_id").on(table.hostId), - ], -); - -export const sessionShareParticipants = pgTable( - "session_share_participants", - { - id: serial("id").primaryKey(), - shareId: varchar("share_id", { length: 255 }) - .notNull() - .references(() => sessionShares.id, { onDelete: "cascade" }), - - userId: varchar("user_id", { length: 255 }).references(() => users.id, { - onDelete: "cascade", - }), - guestLabel: text("guest_label"), - - joinedAt: text("joined_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - leftAt: text("left_at"), - }, -); - export const opksshTokens = pgTable( "opkssh_tokens", { @@ -1570,81 +1506,6 @@ export const aiProposals = pgTable( // --- collab rooms --- -/** - * A collaboration room: a group of users watching one "stage" - the live - * session the current presenter is showing. The stage points at a - * shareType="room" row in session_shares, so transport, gating, recording and - * expiry all reuse the session-sharing machinery. - */ -export const collabRooms = pgTable( - "collab_rooms", - { - id: varchar("id", { length: 255 }).primaryKey(), - name: varchar("name", { length: 255 }).notNull(), - ownerUserId: varchar("owner_user_id", { length: 255 }) - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - - // Persistent rooms survive being emptied and can be re-used; one-off - // rooms are ended explicitly and never listed again. - persistent: boolean("persistent") - .notNull() - .default(false), - - presenterUserId: varchar("presenter_user_id", { length: 255 }).references(() => users.id, { - onDelete: "set null", - }), - stageProtocol: text("stage_protocol"), - stageHostId: integer("stage_host_id").references(() => hosts.id, { - onDelete: "set null", - }), - stageShareId: varchar("stage_share_id", { length: 255 }).references(() => sessionShares.id, { - onDelete: "set null", - }), - - // Set = anonymous guests may watch the stage through this token. - guestLinkToken: varchar("guest_link_token", { length: 255 }), - - createdAt: varchar("created_at", { length: 255 }) - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - endedAt: text("ended_at"), - }, - (table) => [ - index("idx_collab_rooms_owner").on(table.ownerUserId), - uniqueIndex("idx_collab_rooms_guest_token").on(table.guestLinkToken), - ], -); - -export const collabRoomMembers = pgTable( - "collab_room_members", - { - id: serial("id").primaryKey(), - roomId: varchar("room_id", { length: 255 }) - .notNull() - .references(() => collabRooms.id, { onDelete: "cascade" }), - userId: varchar("user_id", { length: 255 }) - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - - // "host" runs the room: invites, force-switches the presenter, ends it. - roomRole: text("room_role").notNull().default("member"), - addedBy: varchar("added_by", { length: 255 }).references(() => users.id, { - onDelete: "set null", - }), - createdAt: varchar("created_at", { length: 255 }) - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - }, - (table) => [ - uniqueIndex("idx_collab_room_members_room_user").on( - table.roomId, - table.userId, - ), - index("idx_collab_room_members_user").on(table.userId), - ], -); - // --- secret sources --- /** diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index db2b633a0..435070c42 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -756,70 +756,6 @@ export const sessionRecordings = sqliteTable( ], ); -export const sessionShares = sqliteTable( - "session_shares", - { - id: text("id").primaryKey(), - - hostId: integer("host_id") - .notNull() - .references(() => hosts.id, { onDelete: "cascade" }), - ownerUserId: text("owner_user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - - protocol: text("protocol").notNull(), - - // Live-session binding: TerminalSessionManager's session.id for SSH, or - // guacd's own guacamoleConnectionId for rdp/vnc/telnet. Neither is a DB - // row (process-local, in-memory) so this intentionally has no FK. - sessionId: text("session_id").notNull(), - tabInstanceId: text("tab_instance_id"), - - shareType: text("share_type").notNull(), // "link" | "user" - targetUserId: text("target_user_id").references(() => users.id, { - onDelete: "cascade", - }), - linkToken: text("link_token").unique(), - - permissionLevel: text("permission_level").notNull().default("read-only"), - - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - expiresAt: text("expires_at").notNull(), - revokedAt: text("revoked_at"), - - lastJoinedAt: text("last_joined_at"), - joinCount: integer("join_count").notNull().default(0), - }, - // Resolved from the live session on join, and listed per host. - (table) => [ - index("idx_session_shares_session_id").on(table.sessionId), - index("idx_session_shares_host_id").on(table.hostId), - ], -); - -export const sessionShareParticipants = sqliteTable( - "session_share_participants", - { - id: integer("id").primaryKey({ autoIncrement: true }), - shareId: text("share_id") - .notNull() - .references(() => sessionShares.id, { onDelete: "cascade" }), - - userId: text("user_id").references(() => users.id, { - onDelete: "cascade", - }), - guestLabel: text("guest_label"), - - joinedAt: text("joined_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - leftAt: text("left_at"), - }, -); - export const opksshTokens = sqliteTable( "opkssh_tokens", { @@ -1568,81 +1504,6 @@ export const aiProposals = sqliteTable( // --- collab rooms --- -/** - * A collaboration room: a group of users watching one "stage" - the live - * session the current presenter is showing. The stage points at a - * shareType="room" row in session_shares, so transport, gating, recording and - * expiry all reuse the session-sharing machinery. - */ -export const collabRooms = sqliteTable( - "collab_rooms", - { - id: text("id").primaryKey(), - name: text("name").notNull(), - ownerUserId: text("owner_user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - - // Persistent rooms survive being emptied and can be re-used; one-off - // rooms are ended explicitly and never listed again. - persistent: integer("persistent", { mode: "boolean" }) - .notNull() - .default(false), - - presenterUserId: text("presenter_user_id").references(() => users.id, { - onDelete: "set null", - }), - stageProtocol: text("stage_protocol"), - stageHostId: integer("stage_host_id").references(() => hosts.id, { - onDelete: "set null", - }), - stageShareId: text("stage_share_id").references(() => sessionShares.id, { - onDelete: "set null", - }), - - // Set = anonymous guests may watch the stage through this token. - guestLinkToken: text("guest_link_token"), - - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - endedAt: text("ended_at"), - }, - (table) => [ - index("idx_collab_rooms_owner").on(table.ownerUserId), - uniqueIndex("idx_collab_rooms_guest_token").on(table.guestLinkToken), - ], -); - -export const collabRoomMembers = sqliteTable( - "collab_room_members", - { - id: integer("id").primaryKey({ autoIncrement: true }), - roomId: text("room_id") - .notNull() - .references(() => collabRooms.id, { onDelete: "cascade" }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - - // "host" runs the room: invites, force-switches the presenter, ends it. - roomRole: text("room_role").notNull().default("member"), - addedBy: text("added_by").references(() => users.id, { - onDelete: "set null", - }), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - }, - (table) => [ - uniqueIndex("idx_collab_room_members_room_user").on( - table.roomId, - table.userId, - ), - index("idx_collab_room_members_user").on(table.userId), - ], -); - // --- secret sources --- /** diff --git a/src/backend/database/repositories/collab-room-repository.ts b/src/backend/database/repositories/collab-room-repository.ts deleted file mode 100644 index 0d212fe31..000000000 --- a/src/backend/database/repositories/collab-room-repository.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { and, desc, eq, isNull } from "drizzle-orm"; -import { collabRoomMembers, collabRooms, users } from "../db/schema.js"; -import type { DatabaseContext } from "./database-context.js"; -import { insertReturning } from "./returning.js"; -import { rowsAffected } from "./mutation-result.js"; - -export type CollabRoomRecord = typeof collabRooms.$inferSelect; -export type CollabRoomMemberRecord = typeof collabRoomMembers.$inferSelect; - -export type CollabRoomRole = "host" | "member"; - -export interface CollabRoomMemberWithUser { - userId: string; - username: string; - roomRole: string; - createdAt: string; -} - -export interface CollabRoomStage { - presenterUserId: string | null; - stageProtocol: string | null; - stageHostId: number | null; - stageShareId: string | null; -} - -export class CollabRoomRepository { - constructor( - private readonly context: DatabaseContext, - private readonly onWrite?: () => void | Promise, - ) {} - - async createRoom(input: { - id: string; - name: string; - ownerUserId: string; - persistent: boolean; - }): Promise { - const [created] = await insertReturning(this.context, collabRooms, { - id: input.id, - name: input.name, - ownerUserId: input.ownerUserId, - persistent: input.persistent, - }); - await this.afterWrite(); - return created; - } - - async findById(id: string): Promise { - const rows = await this.context.drizzle - .select() - .from(collabRooms) - .where(eq(collabRooms.id, id)) - .limit(1); - return rows[0] ?? null; - } - - /** The live room whose stage points at this share, if any. */ - async findByStageShareId(shareId: string): Promise { - const rows = await this.context.drizzle - .select() - .from(collabRooms) - .where( - and(eq(collabRooms.stageShareId, shareId), isNull(collabRooms.endedAt)), - ) - .limit(1); - return rows[0] ?? null; - } - - async findByGuestToken(token: string): Promise { - const rows = await this.context.drizzle - .select() - .from(collabRooms) - .where( - and(eq(collabRooms.guestLinkToken, token), isNull(collabRooms.endedAt)), - ) - .limit(1); - return rows[0] ?? null; - } - - async setGuestToken(roomId: string, token: string | null): Promise { - await this.context.drizzle - .update(collabRooms) - .set({ guestLinkToken: token }) - .where(eq(collabRooms.id, roomId)); - await this.afterWrite(); - } - - async listForUser(userId: string): Promise { - const rows = await this.context.drizzle - .select({ room: collabRooms }) - .from(collabRoomMembers) - .innerJoin(collabRooms, eq(collabRoomMembers.roomId, collabRooms.id)) - .where( - and(eq(collabRoomMembers.userId, userId), isNull(collabRooms.endedAt)), - ) - .orderBy(desc(collabRooms.createdAt)); - return rows.map((row) => row.room); - } - - async findMember( - roomId: string, - userId: string, - ): Promise { - const rows = await this.context.drizzle - .select() - .from(collabRoomMembers) - .where( - and( - eq(collabRoomMembers.roomId, roomId), - eq(collabRoomMembers.userId, userId), - ), - ) - .limit(1); - return rows[0] ?? null; - } - - async addMember(input: { - roomId: string; - userId: string; - roomRole: CollabRoomRole; - addedBy: string | null; - }): Promise { - if (await this.findMember(input.roomId, input.userId)) return false; - await this.context.drizzle.insert(collabRoomMembers).values({ - roomId: input.roomId, - userId: input.userId, - roomRole: input.roomRole, - addedBy: input.addedBy, - }); - await this.afterWrite(); - return true; - } - - async removeMember(roomId: string, userId: string): Promise { - await this.context.drizzle - .delete(collabRoomMembers) - .where( - and( - eq(collabRoomMembers.roomId, roomId), - eq(collabRoomMembers.userId, userId), - ), - ); - await this.afterWrite(); - } - - async listMembers(roomId: string): Promise { - return this.context.drizzle - .select({ - userId: collabRoomMembers.userId, - username: users.username, - roomRole: collabRoomMembers.roomRole, - createdAt: collabRoomMembers.createdAt, - }) - .from(collabRoomMembers) - .innerJoin(users, eq(collabRoomMembers.userId, users.id)) - .where(eq(collabRoomMembers.roomId, roomId)) - .orderBy(users.username); - } - - async updateStage(roomId: string, stage: CollabRoomStage): Promise { - await this.context.drizzle - .update(collabRooms) - .set(stage) - .where(eq(collabRooms.id, roomId)); - await this.afterWrite(); - } - - async replaceStage( - roomId: string, - expectedShareId: string | null, - stage: CollabRoomStage, - ): Promise { - const result = await this.context.drizzle - .update(collabRooms) - .set(stage) - .where( - and( - eq(collabRooms.id, roomId), - expectedShareId - ? eq(collabRooms.stageShareId, expectedShareId) - : isNull(collabRooms.stageShareId), - ), - ); - const changed = rowsAffected(result) > 0; - if (changed) await this.afterWrite(); - return changed; - } - - async clearStage(roomId: string): Promise { - return this.updateStage(roomId, { - presenterUserId: null, - stageProtocol: null, - stageHostId: null, - stageShareId: null, - }); - } - - async endRoom(roomId: string): Promise { - await this.context.drizzle - .update(collabRooms) - .set({ - endedAt: new Date().toISOString(), - presenterUserId: null, - stageProtocol: null, - stageHostId: null, - stageShareId: null, - }) - .where(eq(collabRooms.id, roomId)); - await this.afterWrite(); - } - - async deleteRoom(roomId: string): Promise { - await this.context.drizzle - .delete(collabRooms) - .where(eq(collabRooms.id, roomId)); - await this.afterWrite(); - } - - private async afterWrite(): Promise { - await this.onWrite?.(); - } -} diff --git a/src/backend/database/repositories/factory.ts b/src/backend/database/repositories/factory.ts index 4685f51ed..4b0e669b0 100644 --- a/src/backend/database/repositories/factory.ts +++ b/src/backend/database/repositories/factory.ts @@ -39,8 +39,6 @@ import { RecentActivityRepository } from "./recent-activity-repository.js"; import { RoleRepository } from "./role-repository.js"; import { SessionRecordingRepository } from "./session-recording-repository.js"; import { SessionRepository } from "./session-repository.js"; -import { SessionShareRepository } from "./session-share-repository.js"; -import { CollabRoomRepository } from "./collab-room-repository.js"; import { SecretSourceRepository } from "./secret-source-repository.js"; import { CredentialAccessRepository } from "./credential-access-repository.js"; import { SharedCredentialSecretsRepository } from "./shared-credential-secrets-repository.js"; @@ -416,20 +414,6 @@ export function createCurrentSessionRepository(): SessionRepository { ); } -export function createCurrentCollabRoomRepository(): CollabRoomRepository { - return new CollabRoomRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("collab_room_repository_write"), - ); -} - -export function createCurrentSessionShareRepository(): SessionShareRepository { - return new SessionShareRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("session_share_repository_write"), - ); -} - export function createCurrentSettingsRepository(): SettingsRepository { return new SettingsRepository( createCurrentRepositoryContext(), diff --git a/src/backend/database/repositories/session-share-repository.ts b/src/backend/database/repositories/session-share-repository.ts deleted file mode 100644 index 3a8dcfef3..000000000 --- a/src/backend/database/repositories/session-share-repository.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { and, eq, gt, isNull, lt } from "drizzle-orm"; -import { - hosts, - sessionShareParticipants, - sessionShares, - users, -} from "../db/schema.js"; -import type { DatabaseContext } from "./database-context.js"; -import { rowsAffected } from "./mutation-result.js"; -import { insertReturning } from "./returning.js"; - -export type SessionShareRecord = typeof sessionShares.$inferSelect; -export type SessionShareParticipantRecord = - typeof sessionShareParticipants.$inferSelect; - -// "room" shares are minted internally by the collab-room routes and are -// joinable by any member of the room whose stage references them. -export type SessionShareType = "link" | "user" | "room"; -export type SessionSharePermissionLevel = "read-only" | "read-write"; - -export interface SessionShareCreateInput { - id: string; - hostId: number; - ownerUserId: string; - protocol: string; - sessionId: string; - tabInstanceId?: string | null; - shareType: SessionShareType; - targetUserId?: string | null; - linkToken?: string | null; - permissionLevel: SessionSharePermissionLevel; - expiresAt: string; -} - -export interface SessionShareWithHost extends SessionShareRecord { - hostName: string | null; - ownerUsername: string | null; -} - -export interface SharedWithMeRecord extends SessionShareRecord { - hostName: string | null; - ownerUsername: string | null; -} - -function activeShareFilter(now: string) { - return and(isNull(sessionShares.revokedAt), gt(sessionShares.expiresAt, now)); -} - -export class SessionShareRepository { - constructor( - private readonly context: DatabaseContext, - private readonly onWrite?: () => void | Promise, - ) {} - - async create(input: SessionShareCreateInput): Promise { - const [created] = await insertReturning(this.context, sessionShares, { - id: input.id, - hostId: input.hostId, - ownerUserId: input.ownerUserId, - protocol: input.protocol, - sessionId: input.sessionId, - tabInstanceId: input.tabInstanceId ?? null, - shareType: input.shareType, - targetUserId: input.targetUserId ?? null, - linkToken: input.linkToken ?? null, - permissionLevel: input.permissionLevel, - expiresAt: input.expiresAt, - }); - - await this.afterWrite(); - return created; - } - - async findById(id: string): Promise { - const rows = await this.context.drizzle - .select() - .from(sessionShares) - .where(eq(sessionShares.id, id)) - .limit(1); - return rows[0] ?? null; - } - - async findActiveById( - id: string, - now = new Date().toISOString(), - ): Promise { - const rows = await this.context.drizzle - .select() - .from(sessionShares) - .where(and(eq(sessionShares.id, id), activeShareFilter(now))) - .limit(1); - return rows[0] ?? null; - } - - async findByLinkToken( - linkToken: string, - now = new Date().toISOString(), - ): Promise { - const rows = await this.context.drizzle - .select() - .from(sessionShares) - .where( - and(eq(sessionShares.linkToken, linkToken), activeShareFilter(now)), - ) - .limit(1); - return rows[0] ?? null; - } - - async findActiveSharesForHost( - hostId: number, - ownerUserId: string, - now = new Date().toISOString(), - ): Promise { - return this.context.drizzle - .select() - .from(sessionShares) - .where( - and( - eq(sessionShares.hostId, hostId), - eq(sessionShares.ownerUserId, ownerUserId), - activeShareFilter(now), - ), - ); - } - - async findSharesTargetingUser( - userId: string, - now = new Date().toISOString(), - ): Promise { - const rows = await this.context.drizzle - .select({ - share: sessionShares, - hostName: hosts.name, - ownerUsername: users.username, - }) - .from(sessionShares) - .leftJoin(hosts, eq(sessionShares.hostId, hosts.id)) - .leftJoin(users, eq(sessionShares.ownerUserId, users.id)) - .where( - and( - eq(sessionShares.shareType, "user"), - eq(sessionShares.targetUserId, userId), - activeShareFilter(now), - ), - ); - - return rows.map((row) => ({ - ...row.share, - hostName: row.hostName, - ownerUsername: row.ownerUsername, - })); - } - - async revoke(shareId: string, requestingUserId: string): Promise { - const result = await this.context.drizzle - .update(sessionShares) - .set({ revokedAt: new Date().toISOString() }) - .where( - and( - eq(sessionShares.id, shareId), - eq(sessionShares.ownerUserId, requestingUserId), - ), - ); - - if (rowsAffected(result) > 0) { - await this.afterWrite(); - } - return rowsAffected(result) > 0; - } - - async revokeAsAdmin(shareId: string): Promise { - const result = await this.context.drizzle - .update(sessionShares) - .set({ revokedAt: new Date().toISOString() }) - .where(eq(sessionShares.id, shareId)); - - if (rowsAffected(result) > 0) { - await this.afterWrite(); - } - return rowsAffected(result) > 0; - } - - async deleteExpiredShares(now = new Date().toISOString()): Promise { - const result = await this.context.drizzle - .delete(sessionShares) - .where(lt(sessionShares.expiresAt, now)); - - if (rowsAffected(result) > 0) { - await this.afterWrite(); - } - return rowsAffected(result); - } - - async touchShareUsage( - shareId: string, - lastJoinedAt = new Date().toISOString(), - ): Promise { - const current = await this.findById(shareId); - await this.context.drizzle - .update(sessionShares) - .set({ - lastJoinedAt, - joinCount: (current?.joinCount ?? 0) + 1, - }) - .where(eq(sessionShares.id, shareId)); - await this.afterWrite(); - } - - async recordParticipantJoin( - shareId: string, - userId: string | null, - guestLabel: string | null, - ): Promise { - const [created] = await insertReturning( - this.context, - sessionShareParticipants, - { shareId, userId, guestLabel }, - ); - await this.afterWrite(); - return created; - } - - async recordParticipantLeave(participantId: number): Promise { - await this.context.drizzle - .update(sessionShareParticipants) - .set({ leftAt: new Date().toISOString() }) - .where(eq(sessionShareParticipants.id, participantId)); - await this.afterWrite(); - } - - async deleteSharesForHost(hostId: number): Promise { - const result = await this.context.drizzle - .delete(sessionShares) - .where(eq(sessionShares.hostId, hostId)); - - if (rowsAffected(result) > 0) { - await this.afterWrite(); - } - return rowsAffected(result); - } - - private async afterWrite(): Promise { - await this.onWrite?.(); - } -} diff --git a/src/backend/database/routes/host.ts b/src/backend/database/routes/host.ts index f03b8555d..26a77feec 100644 --- a/src/backend/database/routes/host.ts +++ b/src/backend/database/routes/host.ts @@ -200,7 +200,6 @@ router.post( enableTmuxMonitor, enableTerminalToolbar, enableAiAssistant, - allowSessionSharing, showTerminalInSidebar, showFileManagerInSidebar, showTunnelInSidebar, @@ -343,7 +342,6 @@ router.post( enableTmuxMonitor: enableTmuxMonitor ? 1 : 0, enableTerminalToolbar: enableTerminalToolbar === false ? 0 : 1, enableAiAssistant: enableAiAssistant ? 1 : 0, - allowSessionSharing: allowSessionSharing === false ? 0 : 1, showTerminalInSidebar: showTerminalInSidebar ? 1 : 0, showFileManagerInSidebar: showFileManagerInSidebar ? 1 : 0, showTunnelInSidebar: showTunnelInSidebar ? 1 : 0, @@ -918,7 +916,6 @@ router.put( enableTmuxMonitor, enableTerminalToolbar, enableAiAssistant, - allowSessionSharing, showTerminalInSidebar, showFileManagerInSidebar, showTunnelInSidebar, @@ -1064,7 +1061,6 @@ router.put( enableTmuxMonitor: enableTmuxMonitor ? 1 : 0, enableTerminalToolbar: enableTerminalToolbar === false ? 0 : 1, enableAiAssistant: enableAiAssistant ? 1 : 0, - allowSessionSharing: allowSessionSharing === false ? 0 : 1, showTerminalInSidebar: showTerminalInSidebar ? 1 : 0, showFileManagerInSidebar: showFileManagerInSidebar ? 1 : 0, showTunnelInSidebar: showTunnelInSidebar ? 1 : 0, diff --git a/src/backend/database/routes/open-tabs.ts b/src/backend/database/routes/open-tabs.ts index 481eabc99..1cf1ee834 100644 --- a/src/backend/database/routes/open-tabs.ts +++ b/src/backend/database/routes/open-tabs.ts @@ -3,10 +3,7 @@ import express, { type Request, type Response } from "express"; import { databaseLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { liveTerminalSessions } from "../../hosts/live-terminal-sessions.js"; -import { - createCurrentOpenTabRepository, - createCurrentSessionShareRepository, -} from "../repositories/factory.js"; +import { createCurrentOpenTabRepository } from "../repositories/factory.js"; const router = express.Router(); const authManager = AuthManager.getInstance(); @@ -298,14 +295,14 @@ router.get( * get: * summary: Get all active backend sessions for the current user * description: > - * Returns live terminal sessions from the session manager, both sessions the - * caller owns and SSH sessions shared to the caller by another user (via - * an in-app session share). Used by the Active Connections panel and tab restore logic. + * Returns the live terminal sessions the caller owns. Sessions other + * users shared with the caller come from the session sharing plugin. + * Used by the Active Connections panel and tab restore logic. * tags: * - Open Tabs * responses: * 200: - * description: List of active sessions (own and shared-with-me). + * description: List of the caller's active sessions. * content: * application/json: * schema: @@ -357,28 +354,6 @@ router.get( shareId: null as string | null, })); - const sharedWithMe = - await createCurrentSessionShareRepository().findSharesTargetingUser( - userId, - ); - for (const share of sharedWithMe) { - if (share.protocol !== "ssh") continue; - const sharedSession = liveTerminalSessions.getSession(share.sessionId); - if (!sharedSession || !sharedSession.isConnected) continue; - result.push({ - sessionId: sharedSession.id, - hostId: sharedSession.hostId, - hostName: sharedSession.hostName, - tabInstanceId: sharedSession.tabInstanceId, - isConnected: sharedSession.isConnected, - createdAt: sharedSession.createdAt, - isOwnSession: false, - sharedByUsername: share.ownerUsername, - permissionLevel: share.permissionLevel, - shareId: share.id, - }); - } - return res.json(result); } catch (e) { databaseLogger.error("Failed to get active sessions", e, { diff --git a/src/backend/database/routes/plugins.ts b/src/backend/database/routes/plugins.ts index cdadd822e..199b5fe9f 100644 --- a/src/backend/database/routes/plugins.ts +++ b/src/backend/database/routes/plugins.ts @@ -54,7 +54,8 @@ const requireManagePlugins = permissionManager.requirePermission( * No auth. A shared-session or collab guest link has no session, yet its * page still draws surfaces a plugin provides (a remote desktop stream). * Returns only enabled plugins whose manifest sets contributes.guest, - * with the fields the browser loader needs and nothing operational. + * with the fields the browser loader needs and nothing operational, + * plus the ?view= names each serves to guests (contributes.guestViews). * tags: * - Plugins * responses: @@ -69,7 +70,7 @@ router.get("/public", async (_req: Request, res: Response) => { if (record.state !== "enabled") return []; const loaded = loader.get(record.id); let manifest: { - contributes?: { guest?: boolean }; + contributes?: { guest?: boolean; guestViews?: string[] }; dependencies?: Record; optionalDependencies?: Record; }; @@ -86,7 +87,10 @@ router.get("/public", async (_req: Request, res: Response) => { version: record.version, enabled: true, state: loaded?.state ?? record.state, - contributes: { guest: true }, + contributes: { + guest: true, + guestViews: manifest.contributes.guestViews ?? [], + }, dependencies: manifest.dependencies ?? {}, optionalDependencies: manifest.optionalDependencies ?? {}, ...describePluginFrontend(loaded), diff --git a/src/backend/database/routes/user-settings-routes.ts b/src/backend/database/routes/user-settings-routes.ts index 5474c5696..89c1c6bfa 100644 --- a/src/backend/database/routes/user-settings-routes.ts +++ b/src/backend/database/routes/user-settings-routes.ts @@ -587,91 +587,6 @@ export function registerUserSettingsRoutes( } }); - router.get("/session-sharing-enabled", authenticateJWT, async (_req, res) => { - try { - res.json({ - enabled: await createCurrentSettingsRepository().getBoolean( - "session_sharing_globally_enabled", - true, - ), - }); - } catch (err) { - authLogger.error("Failed to get session sharing enabled setting", err); - res - .status(500) - .json({ error: "Failed to get session sharing enabled setting" }); - } - }); - - /** - * @openapi - * /users/session-sharing-enabled: - * patch: - * summary: Update session sharing globally enabled setting (admin only) - * description: Enables or disables live session sharing instance-wide, overriding every per-host allowSessionSharing toggle. - * tags: - * - Users - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * enabled: - * type: boolean - * responses: - * 200: - * description: Setting updated. - * 403: - * description: Not authorized. - * 500: - * description: Failed to update setting. - */ - router.patch( - "/session-sharing-enabled", - authenticateJWT, - async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - try { - const actor = await getAdminActor(userId); - if (!actor) { - return res.status(403).json({ error: "Not authorized" }); - } - const { enabled } = req.body; - if (typeof enabled !== "boolean") { - return res.status(400).json({ error: "enabled must be a boolean" }); - } - await createCurrentSettingsRepository().set( - "session_sharing_globally_enabled", - enabled ? "true" : "false", - ); - - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: actor.username ?? userId, - action: "update_session_sharing_enabled", - resourceType: "setting", - details: JSON.stringify({ enabled }), - ipAddress, - userAgent, - success: true, - }); - - res.json({ enabled }); - } catch (err) { - authLogger.error( - "Failed to update session sharing enabled setting", - err, - ); - res - .status(500) - .json({ error: "Failed to update session sharing enabled setting" }); - } - }, - ); - /** * GET/PATCH a comma-list of private hosts an outbound feature may reach. * Shared by notifications and Step CA; each keeps its own setting key. diff --git a/src/backend/hosts/collab/guest-rate-limit.ts b/src/backend/hosts/collab/guest-rate-limit.ts deleted file mode 100644 index 0d488d11f..000000000 --- a/src/backend/hosts/collab/guest-rate-limit.ts +++ /dev/null @@ -1,21 +0,0 @@ -const WINDOW_MS = 60_000; -const MAX_ATTEMPTS = 60; -const attempts = new Map(); - -export function isCollabGuestRateLimited(ip: string): boolean { - const now = Date.now(); - const entry = attempts.get(ip); - if (!entry || now - entry.windowStart > WINDOW_MS) { - attempts.set(ip, { count: 1, windowStart: now }); - return false; - } - entry.count += 1; - return entry.count > MAX_ATTEMPTS; -} - -setInterval(() => { - const now = Date.now(); - for (const [ip, entry] of attempts) { - if (now - entry.windowStart > WINDOW_MS) attempts.delete(ip); - } -}, 5 * WINDOW_MS).unref(); diff --git a/src/backend/hosts/collab/room-share-access.ts b/src/backend/hosts/collab/room-share-access.ts deleted file mode 100644 index 217e33fe4..000000000 --- a/src/backend/hosts/collab/room-share-access.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createCurrentCollabRoomRepository } from "../../database/repositories/factory.js"; - -/** - * Whether a user may join a room-stage share: the share must be the stage of - * a live room they are a member of. Membership is the authorization - room - * stages are read-only and never expose host credentials or config. - */ -export async function canJoinRoomStageShare( - shareId: string, - userId: string, -): Promise { - const repository = createCurrentCollabRoomRepository(); - const room = await repository.findByStageShareId(shareId); - if (!room) return false; - return !!(await repository.findMember(room.id, userId)); -} diff --git a/src/backend/hosts/collab/routes.ts b/src/backend/hosts/collab/routes.ts deleted file mode 100644 index 90e104593..000000000 --- a/src/backend/hosts/collab/routes.ts +++ /dev/null @@ -1,1239 +0,0 @@ -import crypto from "crypto"; -import express, { type Request, type Response } from "express"; -import type { AuthenticatedRequest } from "../../../types/index.js"; -import { AuthManager } from "../../utils/auth-manager.js"; -import { sshLogger } from "../../utils/logger.js"; -import { - logAudit, - getAuditUsername, - getRequestMeta, -} from "../../utils/audit-logger.js"; -// Remote Desktop is a first-party plugin (plugins/remote-desktop); this goes -// through the same in-core bridge its router is mounted behind, since -// plugins/ is compiled separately from src/backend/ and core cannot -// statically import across that boundary. See -// src/backend/database/routes/guacamole-dispatch.ts and that plugin's -// README -- this is an accepted, temporary coupling until collab becomes a -// plugin of its own. -import { createGuacamoleJoinToken } from "../guacamole-sessions.js"; -import { collabRoomHub } from "./room-hub.js"; -import { getStageController, setStageController } from "./stage-control.js"; -import { isCollabGuestRateLimited } from "./guest-rate-limit.js"; -import { - collabRuntimeStore, - type CollabControlRequest, -} from "./runtime-store.js"; -import { liveTerminalSessions } from "../live-terminal-sessions.js"; -import { - isLiveSession, - isLiveSessionOwnedBy, - isSharingEnabledForHost, - type LiveProtocol, -} from "../session-sharing/live-sessions.js"; -import { - createCurrentCollabRoomRepository, - createCurrentRoleRepository, - createCurrentSessionShareRepository, - createCurrentUserRepository, -} from "../../database/repositories/factory.js"; -import type { CollabRoomRecord } from "../../database/repositories/collab-room-repository.js"; - -/* - * Known limits, shared with session sharing v1: - * - Redis synchronizes collaboration events and ephemeral state across - * instances, but live session transports still require WebSocket affinity. - * - Guacamole stages stay read-only because guacamole-lite cannot revoke a - * writable viewer without disconnecting the whole shared session. - */ -const router = express.Router(); -const authManager = AuthManager.getInstance(); -const authenticateJWT = authManager.createAuthMiddleware(); - -const STAGE_SHARE_EXPIRY_HOURS = 12; -const MAX_INVITE_TARGETS = 200; -const CONTROL_REQUEST_COOLDOWN_MS = 5000; -const PROTOCOLS: LiveProtocol[] = ["ssh", "rdp", "vnc", "telnet"]; - -function isNonEmptyString(value: unknown): value is string { - return typeof value === "string" && value.trim().length > 0; -} - -async function requireRoomMember( - roomId: string, - userId: string, -): Promise<{ room: CollabRoomRecord; isHost: boolean } | null> { - const repository = createCurrentCollabRoomRepository(); - const room = await repository.findById(roomId); - if (!room || room.endedAt) return null; - const member = await repository.findMember(roomId, userId); - if (!member) return null; - return { room, isHost: member.roomRole === "host" }; -} - -async function revokeStageShare(room: CollabRoomRecord): Promise { - if (!room.stageShareId) return; - const repository = createCurrentSessionShareRepository(); - const share = await repository.findActiveById(room.stageShareId); - if (!share) return; - if (!(await repository.revokeAsAdmin(room.stageShareId))) { - throw new Error("Failed to revoke the active stage share"); - } - if (share.protocol === "ssh") { - liveTerminalSessions.disconnectShareParticipants( - share.sessionId, - share.id, - { - reason: "The collaboration stage ended", - }, - ); - void collabRuntimeStore.publish(room.id, { - type: "collab_internal_stage_revoked", - sessionId: share.sessionId, - shareId: share.id, - }); - } -} - -function publicRoom(room: CollabRoomRecord) { - const { guestLinkToken: _secret, ...safeRoom } = room; - return { ...safeRoom, guestLinkEnabled: Boolean(_secret) }; -} - -function stagePayload(room: CollabRoomRecord) { - return { - presenterUserId: room.presenterUserId, - protocol: room.stageProtocol, - hostId: room.stageHostId, - shareId: room.stageShareId, - }; -} - -/** - * @openapi - * /collab/rooms: - * post: - * summary: Create a collaboration room - * tags: - * - Collab - */ -router.post("/rooms", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId!; - const { name, persistent } = req.body ?? {}; - - if (!isNonEmptyString(name) || name.trim().length > 120) { - return res.status(400).json({ error: "Room name is required" }); - } - - try { - const repository = createCurrentCollabRoomRepository(); - const room = await repository.createRoom({ - id: crypto.randomUUID(), - name: name.trim(), - ownerUserId: userId, - persistent: persistent === true, - }); - await repository.addMember({ - roomId: room.id, - userId, - roomRole: "host", - addedBy: userId, - }); - - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: await getAuditUsername(userId), - action: "collab_room_create", - resourceType: "collab_room", - resourceId: room.id, - resourceName: room.name, - ipAddress, - userAgent, - success: true, - }); - - res.json({ room: publicRoom(room) }); - } catch (error) { - sshLogger.error("Failed to create collab room", error, { - operation: "collab_room_create_error", - }); - res.status(500).json({ error: "Failed to create room" }); - } -}); - -/** - * @openapi - * /collab/rooms: - * get: - * summary: List rooms the caller belongs to - * tags: - * - Collab - */ -router.get("/rooms", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId!; - try { - const rooms = await createCurrentCollabRoomRepository().listForUser(userId); - res.json({ rooms: rooms.map(publicRoom) }); - } catch (error) { - sshLogger.error("Failed to list collab rooms", error, { - operation: "collab_room_list_error", - }); - res.status(500).json({ error: "Failed to list rooms" }); - } -}); - -/** - * @openapi - * /collab/rooms/{id}: - * get: - * summary: Get a room with members, online users and stage state - * tags: - * - Collab - */ -router.get( - "/rooms/:id", - authenticateJWT, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId!; - const roomId = String(req.params.id); - try { - let access = await requireRoomMember(roomId, userId); - if (!access) { - return res.status(404).json({ error: "Room not found" }); - } - - // The presenter's own client never calls /stage (it drives the stage - // locally instead), so this is the only place that can notice its - // session died - e.g. the tab was closed without hitting "stop - // presenting". Without this, presenterUserId stays stuck pointing at a - // dead session forever, and the presenter's next visit shows the - // "presenting in another tab" dead end with nothing to stop. - if (access.room.stageShareId && access.room.stageProtocol) { - const share = - await createCurrentSessionShareRepository().findActiveById( - access.room.stageShareId, - ); - const protocol = access.room.stageProtocol as LiveProtocol; - if (!share || !isLiveSession(protocol, share.sessionId)) { - await setStageController(roomId, null); - await createCurrentCollabRoomRepository().clearStage(roomId); - collabRoomHub.broadcast(roomId, { - type: "collab_stage_changed", - roomId, - stage: null, - }); - access = await requireRoomMember(roomId, userId); - if (!access) { - return res.status(404).json({ error: "Room not found" }); - } - } - } - - const members = - await createCurrentCollabRoomRepository().listMembers(roomId); - const mayReviewControlRequests = - access.isHost || access.room.presenterUserId === userId; - const controlRequests = await collabRuntimeStore.listRequests(roomId); - res.json({ - room: publicRoom(access.room), - me: userId, - isHost: access.isHost, - members, - online: await collabRoomHub.onlineUsers(roomId), - stage: stagePayload(access.room), - controllerUserId: await getStageController(roomId), - controlRequests: mayReviewControlRequests - ? controlRequests - : controlRequests.filter((request) => request.userId === userId), - // Room events ride on the terminal socket. - eventsWsPath: "/plugin-ws/ssh-terminal/terminal", - }); - } catch (error) { - sshLogger.error("Failed to get collab room", error, { - operation: "collab_room_get_error", - }); - res.status(500).json({ error: "Failed to get room" }); - } - }, -); - -/** - * @openapi - * /collab/rooms/{id}/members: - * post: - * summary: Invite users to a room (host only) - * tags: - * - Collab - */ -router.post( - "/rooms/:id/members", - authenticateJWT, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId!; - const roomId = String(req.params.id); - const { userIds = [], roleIds = [] } = req.body ?? {}; - - if ( - !Array.isArray(userIds) || - userIds.some((id) => !isNonEmptyString(id)) || - !Array.isArray(roleIds) || - roleIds.some((id) => !Number.isInteger(id)) || - (userIds.length === 0 && roleIds.length === 0) - ) { - return res.status(400).json({ - error: "userIds (user ids) or roleIds (integers) are required", - }); - } - if (userIds.length + roleIds.length > MAX_INVITE_TARGETS) { - return res.status(400).json({ error: "Too many invite targets" }); - } - - try { - const access = await requireRoomMember(roomId, userId); - if (!access) { - return res.status(404).json({ error: "Room not found" }); - } - if (!access.isHost) { - return res.status(403).json({ error: "Only the host can invite" }); - } - - const userRepository = createCurrentUserRepository(); - const repository = createCurrentCollabRoomRepository(); - for (const targetId of userIds as string[]) { - if (!(await userRepository.findById(targetId))) { - return res.status(404).json({ error: "User not found", targetId }); - } - } - // Roles expand to their current members - a snapshot, like folder - // sharing; people joining the role later are not pulled in. - const roleRepository = createCurrentRoleRepository(); - const expanded = new Set(userIds as string[]); - for (const roleId of roleIds as number[]) { - if (!(await roleRepository.findRoleById(roleId))) { - return res.status(404).json({ error: "Role not found", roleId }); - } - for (const memberId of await roleRepository.listRoleUserIds(roleId)) { - expanded.add(memberId); - } - } - for (const targetId of expanded) { - await repository.addMember({ - roomId, - userId: targetId, - roomRole: "member", - addedBy: userId, - }); - } - - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: await getAuditUsername(userId), - action: "collab_room_invite", - resourceType: "collab_room", - resourceId: roomId, - resourceName: access.room.name, - details: JSON.stringify({ memberCount: expanded.size }), - ipAddress, - userAgent, - success: true, - }); - - collabRoomHub.broadcast(roomId, { - type: "collab_members_changed", - roomId, - }); - res.json({ success: true }); - } catch (error) { - sshLogger.error("Failed to invite collab room members", error, { - operation: "collab_room_invite_error", - }); - res.status(500).json({ error: "Failed to invite members" }); - } - }, -); - -/** - * @openapi - * /collab/rooms/{id}/members/{userId}: - * delete: - * summary: Remove a member (host), or leave the room (self) - * tags: - * - Collab - */ -router.delete( - "/rooms/:id/members/:userId", - authenticateJWT, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId!; - const roomId = String(req.params.id); - const targetId = String(req.params.userId); - - try { - const access = await requireRoomMember(roomId, userId); - if (!access) { - return res.status(404).json({ error: "Room not found" }); - } - if (targetId !== userId && !access.isHost) { - return res - .status(403) - .json({ error: "Only the host can remove members" }); - } - if (targetId === access.room.ownerUserId) { - return res.status(400).json({ error: "The owner cannot be removed" }); - } - - const repository = createCurrentCollabRoomRepository(); - await repository.removeMember(roomId, targetId); - await collabRuntimeStore.removeRequest(roomId, targetId); - - if (access.room.stageShareId && access.room.stageProtocol === "ssh") { - const share = - await createCurrentSessionShareRepository().findActiveById( - access.room.stageShareId, - ); - if (share) { - liveTerminalSessions.disconnectShareParticipants( - share.sessionId, - share.id, - { - userId: targetId, - reason: "You were removed from the collaboration room", - }, - ); - } - } - - if ((await getStageController(roomId)) === targetId) { - await applyStageControl(access.room, roomId, null); - } - - if (access.room.presenterUserId === targetId) { - await revokeStageShare(access.room); - await repository.clearStage(roomId); - collabRoomHub.broadcast(roomId, { - type: "collab_stage_changed", - roomId, - stage: null, - }); - } - - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: await getAuditUsername(userId), - action: - targetId === userId - ? "collab_room_leave" - : "collab_room_remove_member", - resourceType: "collab_room", - resourceId: roomId, - resourceName: access.room.name, - details: JSON.stringify({ targetUserId: targetId }), - ipAddress, - userAgent, - success: true, - }); - - collabRoomHub.broadcast(roomId, { - type: "collab_members_changed", - roomId, - }); - res.json({ success: true }); - } catch (error) { - sshLogger.error("Failed to remove collab room member", error, { - operation: "collab_room_remove_member_error", - }); - res.status(500).json({ error: "Failed to remove member" }); - } - }, -); - -/** - * @openapi - * /collab/rooms/{id}/present: - * post: - * summary: Take the stage with one of your live sessions - * description: Any member may take over the stage; the previous stage share is revoked. The caller must own the live session and sharing must be enabled for the host. - * tags: - * - Collab - */ -router.post( - "/rooms/:id/present", - authenticateJWT, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId!; - const roomId = String(req.params.id); - const { protocol, sessionId, hostId } = req.body ?? {}; - - if (!PROTOCOLS.includes(protocol)) { - return res.status(400).json({ error: "Invalid protocol" }); - } - if (!isNonEmptyString(sessionId) || !Number.isInteger(Number(hostId))) { - return res - .status(400) - .json({ error: "sessionId and hostId are required" }); - } - - try { - const access = await requireRoomMember(roomId, userId); - if (!access) { - return res.status(404).json({ error: "Room not found" }); - } - - const numericHostId = Number(hostId); - const { enabled } = await isSharingEnabledForHost(numericHostId); - if (!enabled) { - return res - .status(403) - .json({ error: "Session sharing is disabled for this host" }); - } - if (!isLiveSessionOwnedBy(protocol, String(sessionId), userId)) { - return res - .status(403) - .json({ error: "You do not own this live session" }); - } - - const shareRepository = createCurrentSessionShareRepository(); - const share = await shareRepository.create({ - id: crypto.randomUUID(), - hostId: numericHostId, - ownerUserId: userId, - protocol, - sessionId: String(sessionId), - shareType: "room", - permissionLevel: "read-only", - expiresAt: new Date( - Date.now() + STAGE_SHARE_EXPIRY_HOURS * 60 * 60 * 1000, - ).toISOString(), - }); - - await revokeStageShare(access.room); - const repository = createCurrentCollabRoomRepository(); - const replaced = await repository.replaceStage( - roomId, - access.room.stageShareId, - { - presenterUserId: userId, - stageProtocol: protocol, - stageHostId: numericHostId, - stageShareId: share.id, - }, - ); - if (!replaced) { - await shareRepository.revokeAsAdmin(share.id); - return res.status(409).json({ error: "The stage changed; try again" }); - } - await setStageController(roomId, null); - await collabRuntimeStore.clearRequests(roomId); - - const stage = { - presenterUserId: userId, - protocol, - hostId: numericHostId, - shareId: share.id, - }; - collabRoomHub.broadcast(roomId, { - type: "collab_stage_changed", - roomId, - stage, - }); - - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: await getAuditUsername(userId), - action: "collab_room_present", - resourceType: "collab_room", - resourceId: roomId, - resourceName: access.room.name, - details: JSON.stringify({ protocol, hostId: numericHostId }), - ipAddress, - userAgent, - success: true, - }); - - res.json({ stage }); - } catch (error) { - sshLogger.error("Failed to take collab room stage", error, { - operation: "collab_room_present_error", - }); - res.status(500).json({ error: "Failed to start presenting" }); - } - }, -); - -/** - * @openapi - * /collab/rooms/{id}/stop: - * post: - * summary: Stop presenting (presenter or host) - * tags: - * - Collab - */ -router.post( - "/rooms/:id/stop", - authenticateJWT, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId!; - const roomId = String(req.params.id); - try { - const access = await requireRoomMember(roomId, userId); - if (!access) { - return res.status(404).json({ error: "Room not found" }); - } - if (access.room.presenterUserId !== userId && !access.isHost) { - return res - .status(403) - .json({ error: "Only the presenter or host can stop the stage" }); - } - - await revokeStageShare(access.room); - await setStageController(roomId, null); - await collabRuntimeStore.clearRequests(roomId); - await createCurrentCollabRoomRepository().clearStage(roomId); - collabRoomHub.broadcast(roomId, { - type: "collab_stage_changed", - roomId, - stage: null, - }); - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: await getAuditUsername(userId), - action: "collab_room_stop_presenting", - resourceType: "collab_room", - resourceId: roomId, - resourceName: access.room.name, - ipAddress, - userAgent, - success: true, - }); - res.json({ success: true }); - } catch (error) { - sshLogger.error("Failed to stop collab room stage", error, { - operation: "collab_room_stop_error", - }); - res.status(500).json({ error: "Failed to stop presenting" }); - } - }, -); - -/** - * @openapi - * /collab/rooms/{id}/stage: - * get: - * summary: Get connect info for the current stage (members only) - * description: SSH stages are joined over the terminal WS by shareId; guac stages get a freshly minted read-only join token. - * tags: - * - Collab - */ -router.get( - "/rooms/:id/stage", - authenticateJWT, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId!; - const roomId = String(req.params.id); - try { - const access = await requireRoomMember(roomId, userId); - if (!access) { - return res.status(404).json({ error: "Room not found" }); - } - const { room } = access; - if (!room.stageShareId || !room.stageProtocol) { - return res.json({ stage: null }); - } - - const share = await createCurrentSessionShareRepository().findActiveById( - room.stageShareId, - ); - const protocol = room.stageProtocol as LiveProtocol; - if (!share || !isLiveSession(protocol, share.sessionId)) { - // The presenter is gone (expired share or dead session): clear the - // stale stage so the room stops pointing at it. - await setStageController(roomId, null); - await createCurrentCollabRoomRepository().clearStage(roomId); - collabRoomHub.broadcast(roomId, { - type: "collab_stage_changed", - roomId, - stage: null, - }); - return res.json({ stage: null }); - } - - const controllerUserId = await getStageController(roomId); - const stage: Record = { - ...stagePayload(room), - sessionId: share.sessionId, - controllerUserId, - }; - if (protocol !== "ssh") { - stage.connectParams = { - token: createGuacamoleJoinToken(share.sessionId, true), - }; - } - res.json({ stage }); - } catch (error) { - sshLogger.error("Failed to resolve collab room stage", error, { - operation: "collab_room_stage_error", - }); - res.status(500).json({ error: "Failed to resolve stage" }); - } - }, -); - -/** Sets the controller everywhere it lives: memory, live SSH gate, hub. */ -async function applyStageControl( - room: CollabRoomRecord, - roomId: string, - controllerUserId: string | null, -): Promise { - await setStageController(roomId, controllerUserId); - if (room.stageShareId && room.stageProtocol === "ssh") { - try { - const share = await createCurrentSessionShareRepository().findActiveById( - room.stageShareId, - ); - if (share) { - liveTerminalSessions.setRoomShareControl( - share.sessionId, - share.id, - controllerUserId, - ); - } - } catch { - // The gate keeps its previous state; the broadcast still lands. - } - } - collabRoomHub.broadcast(roomId, { - type: "collab_control_changed", - roomId, - controllerUserId, - }); -} - -collabRuntimeStore.onEvent((roomId, message) => { - if (!("type" in message) || typeof message.type !== "string") return; - if (message.type === "collab_control_changed") { - const controllerUserId: string | null = - "controllerUserId" in message && - typeof message.controllerUserId === "string" - ? message.controllerUserId - : null; - void createCurrentCollabRoomRepository() - .findById(roomId) - .then((room) => { - if (!room?.stageShareId || room.stageProtocol !== "ssh") return; - return createCurrentSessionShareRepository() - .findActiveById(room.stageShareId) - .then((share) => { - if (share) { - liveTerminalSessions.setRoomShareControl( - share.sessionId, - share.id, - controllerUserId, - ); - } - }); - }) - .catch(() => {}); - } - if ( - message.type === "collab_internal_stage_revoked" && - "sessionId" in message && - typeof message.sessionId === "string" && - "shareId" in message && - typeof message.shareId === "string" - ) { - liveTerminalSessions.disconnectShareParticipants( - message.sessionId, - message.shareId, - { - reason: "The collaboration stage ended", - }, - ); - } -}); - -/** - * @openapi - * /collab/rooms/{id}/control: - * post: - * summary: Grant or revoke stage control (presenter or host) - * description: Grants a member write access to the current stage, or revokes it with a null userId. The controller may also release control themselves. - * tags: - * - Collab - */ -router.post( - "/rooms/:id/control", - authenticateJWT, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId!; - const roomId = String(req.params.id); - const { userId: targetId } = req.body ?? {}; - - if (targetId !== null && !isNonEmptyString(targetId)) { - return res - .status(400) - .json({ error: "userId must be a user id or null" }); - } - - try { - const access = await requireRoomMember(roomId, userId); - if (!access) { - return res.status(404).json({ error: "Room not found" }); - } - if (!access.room.stageShareId) { - return res.status(400).json({ error: "Nothing is being presented" }); - } - if (access.room.stageProtocol !== "ssh") { - return res.status(400).json({ - error: "Remote desktop stages are read-only", - }); - } - - const releasingOwnControl = - targetId === null && (await getStageController(roomId)) === userId; - const mayGrant = access.isHost || access.room.presenterUserId === userId; - if (!mayGrant && !releasingOwnControl) { - return res.status(403).json({ - error: "Only the presenter or host can change stage control", - }); - } - - if (targetId) { - const repository = createCurrentCollabRoomRepository(); - if (!(await repository.findMember(roomId, targetId))) { - return res.status(404).json({ error: "Member not found" }); - } - } - - await applyStageControl(access.room, roomId, targetId); - if (targetId) { - await collabRuntimeStore.removeRequest(roomId, targetId); - collabRoomHub.broadcast(roomId, { - type: "collab_control_requests_changed", - roomId, - }); - } - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: await getAuditUsername(userId), - action: targetId ? "collab_control_grant" : "collab_control_revoke", - resourceType: "collab_room", - resourceId: roomId, - resourceName: access.room.name, - details: JSON.stringify({ targetUserId: targetId }), - ipAddress, - userAgent, - success: true, - }); - res.json({ controllerUserId: targetId }); - } catch (error) { - sshLogger.error("Failed to change collab stage control", error, { - operation: "collab_control_error", - }); - res.status(500).json({ error: "Failed to change stage control" }); - } - }, -); - -/** - * @openapi - * /collab/rooms/{id}/control/request: - * post: - * summary: Ask the presenter for stage control (hand raise) - * tags: - * - Collab - */ -router.post( - "/rooms/:id/control/request", - authenticateJWT, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId!; - const roomId = String(req.params.id); - try { - const access = await requireRoomMember(roomId, userId); - if (!access) { - return res.status(404).json({ error: "Room not found" }); - } - if (!access.room.stageShareId) { - return res.status(400).json({ error: "Nothing is being presented" }); - } - if (access.room.stageProtocol !== "ssh") { - return res.status(400).json({ - error: "Remote desktop stages are read-only", - }); - } - const existing = (await collabRuntimeStore.listRequests(roomId)).find( - (request) => request.userId === userId, - ); - if ( - existing && - Date.now() - Date.parse(existing.requestedAt) < - CONTROL_REQUEST_COOLDOWN_MS - ) { - return res.status(429).json({ error: "Control was already requested" }); - } - const request: CollabControlRequest = { - userId, - username: await getAuditUsername(userId), - requestedAt: new Date().toISOString(), - }; - await collabRuntimeStore.upsertRequest(roomId, request); - collabRoomHub.broadcast(roomId, { - type: "collab_control_requested", - roomId, - ...request, - }); - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: await getAuditUsername(userId), - action: "collab_control_request", - resourceType: "collab_room", - resourceId: roomId, - resourceName: access.room.name, - ipAddress, - userAgent, - success: true, - }); - res.json({ request }); - } catch (error) { - sshLogger.error("Failed to request collab stage control", error, { - operation: "collab_control_request_error", - }); - res.status(500).json({ error: "Failed to request control" }); - } - }, -); - -router.get( - "/rooms/:id/control/requests", - authenticateJWT, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId!; - const roomId = String(req.params.id); - try { - const access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - if (!access.isHost && access.room.presenterUserId !== userId) { - return res.status(403).json({ - error: "Only the presenter or host can review control requests", - }); - } - res.json({ requests: await collabRuntimeStore.listRequests(roomId) }); - } catch (error) { - sshLogger.error("Failed to list collab control requests", error, { - operation: "collab_control_requests_list_error", - }); - res.status(500).json({ error: "Failed to list control requests" }); - } - }, -); - -router.delete( - "/rooms/:id/control/requests/:userId", - authenticateJWT, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId!; - const roomId = String(req.params.id); - const targetId = String(req.params.userId); - try { - const access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - const mayReview = access.isHost || access.room.presenterUserId === userId; - if (!mayReview && targetId !== userId) { - return res - .status(403) - .json({ error: "Not allowed to dismiss request" }); - } - await collabRuntimeStore.removeRequest(roomId, targetId); - collabRoomHub.broadcast(roomId, { - type: "collab_control_requests_changed", - roomId, - }); - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: await getAuditUsername(userId), - action: - targetId === userId - ? "collab_control_request_cancel" - : "collab_control_request_dismiss", - resourceType: "collab_room", - resourceId: roomId, - resourceName: access.room.name, - details: JSON.stringify({ targetUserId: targetId }), - ipAddress, - userAgent, - success: true, - }); - res.json({ success: true }); - } catch (error) { - sshLogger.error("Failed to dismiss collab control request", error, { - operation: "collab_control_request_dismiss_error", - }); - res.status(500).json({ error: "Failed to dismiss control request" }); - } - }, -); - -/** - * @openapi - * /collab/rooms/{id}/guest-link: - * post: - * summary: Enable, rotate or disable the room's anonymous guest link (host only) - * tags: - * - Collab - */ -router.post( - "/rooms/:id/guest-link", - authenticateJWT, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId!; - const roomId = String(req.params.id); - const { enabled } = req.body ?? {}; - if (typeof enabled !== "boolean") { - return res.status(400).json({ error: "enabled must be a boolean" }); - } - try { - const access = await requireRoomMember(roomId, userId); - if (!access) { - return res.status(404).json({ error: "Room not found" }); - } - if (!access.isHost) { - return res - .status(403) - .json({ error: "Only the host can manage the guest link" }); - } - const token = enabled - ? crypto.randomBytes(24).toString("base64url") - : null; - await createCurrentCollabRoomRepository().setGuestToken(roomId, token); - if ( - (!enabled || access.room.guestLinkToken) && - access.room.stageShareId && - access.room.stageProtocol === "ssh" - ) { - const share = - await createCurrentSessionShareRepository().findActiveById( - access.room.stageShareId, - ); - if (share) { - liveTerminalSessions.disconnectShareParticipants( - share.sessionId, - share.id, - { - userId: null, - reason: enabled - ? "The guest link was rotated" - : "The guest link was disabled", - }, - ); - } - } - - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: await getAuditUsername(userId), - action: enabled - ? "collab_guest_link_enable" - : "collab_guest_link_disable", - resourceType: "collab_room", - resourceId: roomId, - resourceName: access.room.name, - ipAddress, - userAgent, - success: true, - }); - res.json({ guestLinkToken: token }); - } catch (error) { - sshLogger.error("Failed to update collab guest link", error, { - operation: "collab_guest_link_error", - }); - res.status(500).json({ error: "Failed to update guest link" }); - } - }, -); - -/** - * @openapi - * /collab/guest/{token}: - * get: - * summary: Resolve a room's current stage for an anonymous guest - * description: Public, rate-limited per IP. Guests poll this to follow presenter switches. Never returns host details; SSH stages are joined over the terminal WS with roomGuestToken, guac stages get a read-only join token. - * tags: - * - Collab - */ -router.get("/guest/:token", async (req: Request, res: Response) => { - const ip = req.ip || req.socket.remoteAddress || "unknown"; - if (isCollabGuestRateLimited(ip)) { - return res.status(429).json({ error: "Too many requests" }); - } - const token = String(req.params.token); - try { - const room = - await createCurrentCollabRoomRepository().findByGuestToken(token); - if (!room) { - return res.status(404).json({ error: "Link not found" }); - } - const response: Record = { - roomName: room.name, - stage: null, - }; - if (room.stageShareId && room.stageProtocol) { - const share = await createCurrentSessionShareRepository().findActiveById( - room.stageShareId, - ); - const protocol = room.stageProtocol as LiveProtocol; - if (share && isLiveSession(protocol, share.sessionId)) { - const { enabled } = await isSharingEnabledForHost(share.hostId); - if (enabled) { - response.stage = { - protocol, - shareId: share.id, - ...(protocol === "ssh" - ? { - wsPath: `/plugin-ws/ssh-terminal/terminal?roomGuestToken=${encodeURIComponent(token)}`, - } - : { - connectParams: { - token: createGuacamoleJoinToken(share.sessionId, true), - }, - }), - }; - } - } - } - res.json(response); - } catch (error) { - sshLogger.error("Failed to resolve collab guest link", error, { - operation: "collab_guest_resolve_error", - }); - res.status(500).json({ error: "Failed to resolve guest link" }); - } -}); - -/** - * @openapi - * /collab/rooms/{id}/end: - * post: - * summary: End the meeting (host only) - * description: Clears the stage. One-off rooms are ended for good; persistent rooms stay listed for reuse. - * tags: - * - Collab - */ -router.post( - "/rooms/:id/end", - authenticateJWT, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId!; - const roomId = String(req.params.id); - try { - const access = await requireRoomMember(roomId, userId); - if (!access) { - return res.status(404).json({ error: "Room not found" }); - } - if (!access.isHost) { - return res - .status(403) - .json({ error: "Only the host can end the room" }); - } - - await revokeStageShare(access.room); - await setStageController(roomId, null); - await collabRuntimeStore.clearRequests(roomId); - const repository = createCurrentCollabRoomRepository(); - if (access.room.persistent) { - await repository.setGuestToken(roomId, null); - await repository.clearStage(roomId); - collabRoomHub.broadcast(roomId, { - type: "collab_stage_changed", - roomId, - stage: null, - }); - } else { - await repository.endRoom(roomId); - collabRoomHub.broadcast(roomId, { type: "collab_room_ended", roomId }); - } - - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: await getAuditUsername(userId), - action: "collab_room_end", - resourceType: "collab_room", - resourceId: roomId, - resourceName: access.room.name, - ipAddress, - userAgent, - success: true, - }); - res.json({ success: true }); - } catch (error) { - sshLogger.error("Failed to end collab room", error, { - operation: "collab_room_end_error", - }); - res.status(500).json({ error: "Failed to end room" }); - } - }, -); - -/** - * @openapi - * /collab/rooms/{id}: - * delete: - * summary: Permanently delete a room (host only) - * description: Revokes the active stage and removes the room and its membership. This cannot be undone. - * tags: - * - Collab - */ -router.delete( - "/rooms/:id", - authenticateJWT, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId!; - const roomId = String(req.params.id); - try { - const access = await requireRoomMember(roomId, userId); - if (!access) { - return res.status(404).json({ error: "Room not found" }); - } - if (!access.isHost) { - return res - .status(403) - .json({ error: "Only the host can delete the room" }); - } - - await revokeStageShare(access.room); - await setStageController(roomId, null); - await collabRuntimeStore.clearRequests(roomId); - collabRoomHub.broadcast(roomId, { type: "collab_room_ended", roomId }); - await createCurrentCollabRoomRepository().deleteRoom(roomId); - - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: await getAuditUsername(userId), - action: "collab_room_delete", - resourceType: "collab_room", - resourceId: roomId, - resourceName: access.room.name, - ipAddress, - userAgent, - success: true, - }); - res.json({ success: true }); - } catch (error) { - sshLogger.error("Failed to delete collab room", error, { - operation: "collab_room_delete_error", - }); - res.status(500).json({ error: "Failed to delete room" }); - } - }, -); - -export default router; diff --git a/src/backend/hosts/collab/stage-control.ts b/src/backend/hosts/collab/stage-control.ts deleted file mode 100644 index 5961cf6ac..000000000 --- a/src/backend/hosts/collab/stage-control.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Who besides the presenter may drive the current stage, per room. - * - * Redis-backed when REDIS_URL is configured, with a local fallback for - * single-instance deployments. Every stage switch clears it. - */ -import { collabRuntimeStore } from "./runtime-store.js"; - -export function getStageController(roomId: string): Promise { - return collabRuntimeStore.getController(roomId); -} - -export function setStageController( - roomId: string, - userId: string | null, -): Promise { - return collabRuntimeStore.setController(roomId, userId); -} diff --git a/src/backend/hosts/guacamole-sessions.ts b/src/backend/hosts/guacamole-sessions.ts index 6196e4619..e6ac4b22b 100644 --- a/src/backend/hosts/guacamole-sessions.ts +++ b/src/backend/hosts/guacamole-sessions.ts @@ -1,79 +1,23 @@ /** * What core needs from the Remote Desktop plugin, resolved through the plugin - * registry rather than an import. + * registry rather than an import: restarting its guacd WebSocket server after + * an admin edits the guacd URL setting. * - * collab, session-sharing and the admin guacd setting all need three things - * from a running Guacamole server: mint a join token, look up a live session, - * restart after a settings change. None of them can import the plugin -- core - * cannot statically import something outside its rootDir -- and the plugin - * cannot hand core a module reference without becoming core again. - * - * So the plugin publishes "remote-desktop.sessions" through ctx.registry on - * activate, the registry revokes it on deactivate, and the helpers below - * degrade the way each caller needs when nothing is published: - * - * - a lookup returns null, because "no such live session" and "the plugin is - * off" mean the same thing to a caller asking whether a session is live; - * - a restart is a no-op, because resurrecting a server the plugin does not - * own would be worse than doing nothing; - * - minting a token throws, because a caller about to hand a viewer a join - * link needs to know it failed rather than get an unusable value. - * - * This replaces the guacamole-dispatch bridge that used to live in - * database/routes/. + * The plugin publishes "remote-desktop.sessions" through ctx.registry on + * activate and the registry revokes it on deactivate. A restart while it is + * off does nothing, because resurrecting a server the plugin does not own + * would be worse than doing nothing. */ import { consume } from "../plugins/registry.js"; export const GUACAMOLE_SESSIONS_KEY = "remote-desktop.sessions"; -export interface GuacamoleSessionInfo { - guacamoleConnectionId: string; - hostId: number; - ownerUserId: string; - protocol: string; - openedAt: number; -} - export interface GuacamoleSessionProvider { restart: () => Promise; - createJoinToken: (guacamoleConnectionId: string, readOnly: boolean) => string; - getSessionInfo: ( - guacamoleConnectionId: string, - ) => GuacamoleSessionInfo | null; } -function provider(): GuacamoleSessionProvider | undefined { - return consume(GUACAMOLE_SESSIONS_KEY); -} - -/** - * Restarts the plugin's guacd WebSocket server, e.g. after an admin edits the - * guacd URL setting. A no-op while the plugin is disabled. - */ +/** Restarts the plugin's guacd WebSocket server. A no-op while it is off. */ export async function restartGuacamoleService(): Promise { - await provider()?.restart(); -} - -/** - * Mints a read-only or writable join token for a live Guacamole session, for - * collab and session-sharing to hand an invited viewer. Throws when the plugin - * is not running. - */ -export function createGuacamoleJoinToken( - guacamoleConnectionId: string, - readOnly: boolean, -): string { - const active = provider(); - if (!active) { - throw new Error("Remote Desktop plugin is not available"); - } - return active.createJoinToken(guacamoleConnectionId, readOnly); -} - -/** Looks up a live Guacamole session by guacd's own connection id. */ -export function getGuacamoleSessionInfo( - guacamoleConnectionId: string, -): GuacamoleSessionInfo | null { - return provider()?.getSessionInfo(guacamoleConnectionId) ?? null; + await consume(GUACAMOLE_SESSIONS_KEY)?.restart(); } diff --git a/src/backend/hosts/live-terminal-sessions.ts b/src/backend/hosts/live-terminal-sessions.ts index 851d7cc2e..1cfd476a3 100644 --- a/src/backend/hosts/live-terminal-sessions.ts +++ b/src/backend/hosts/live-terminal-sessions.ts @@ -1,17 +1,17 @@ /** * What core needs from live SSH terminal sessions, through the plugin - * service "sessions.live" rather than an import. + * service "sessions.live" (the "ssh" provider) rather than an import. * - * Session sharing, collab and the open-tabs route still live in core and - * reach the sessions the ssh-terminal plugin holds. With no provider (the - * plugin is off) a lookup finds nothing and a control call does nothing, - * because "no such live session" and "the terminal is off" mean the same - * thing to every caller here. + * The open-tabs route lists the caller's sessions and ties saved tabs to the + * terminal's idle timeout. With no provider (the terminal is off) a lookup + * finds nothing, because "no live session" and "the terminal is off" mean the + * same thing there. */ import { getServiceImplementation } from "../plugins/service-registry.js"; export const LIVE_SESSIONS_SERVICE = "sessions.live"; +const SSH_PROVIDER = "ssh"; export interface LiveSessionInfo { id: string; @@ -28,19 +28,7 @@ export interface LiveSessionInfo { } interface LiveSessionsV1 { - getSession: (sessionId: string) => LiveSessionInfo | null; listForUser: (userId: string) => LiveSessionInfo[]; - ownerEndSession: (sessionId: string, reason: string) => void; - disconnectParticipants: ( - sessionId: string, - shareId: string, - options: { reason: string; userId?: string | null }, - ) => number; - setRoomShareControl: ( - sessionId: string, - shareId: string, - controllerUserId: string | null, - ) => void; idleTimeoutMinutes: () => number; } @@ -48,33 +36,14 @@ function provider(): LiveSessionsV1 | undefined { return getServiceImplementation( LIVE_SESSIONS_SERVICE, "^1.0.0", + SSH_PROVIDER, ); } export const liveTerminalSessions = { - getSession(sessionId: string): LiveSessionInfo | null { - return provider()?.getSession(sessionId) ?? null; - }, listForUser(userId: string): LiveSessionInfo[] { return provider()?.listForUser(userId) ?? []; }, - ownerEndSession(sessionId: string, reason: string): void { - provider()?.ownerEndSession(sessionId, reason); - }, - disconnectShareParticipants( - sessionId: string, - shareId: string, - options: { reason: string; userId?: string | null }, - ): number { - return provider()?.disconnectParticipants(sessionId, shareId, options) ?? 0; - }, - setRoomShareControl( - sessionId: string, - shareId: string, - controllerUserId: string | null, - ): void { - provider()?.setRoomShareControl(sessionId, shareId, controllerUserId); - }, /** How long the terminal keeps a detached session, or null while it is off. */ idleTimeoutMinutes(): number | null { return provider()?.idleTimeoutMinutes() ?? null; diff --git a/src/backend/hosts/session-sharing/live-sessions.ts b/src/backend/hosts/session-sharing/live-sessions.ts deleted file mode 100644 index d61a98019..000000000 --- a/src/backend/hosts/session-sharing/live-sessions.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { liveTerminalSessions } from "../live-terminal-sessions.js"; -// Remote Desktop is a first-party plugin (plugins/remote-desktop); this goes -// through the same in-core bridge its router is mounted behind, since -// plugins/ is compiled separately from src/backend/ and core cannot -// statically import across that boundary. See -// src/backend/database/routes/guacamole-dispatch.ts and that plugin's -// README -- this is an accepted, temporary coupling until collab/ -// session-sharing themselves become plugins. -import { getGuacamoleSessionInfo as getGuacSessionInfo } from "../guacamole-sessions.js"; -import { - createCurrentHostResolutionRepository, - createCurrentSettingsRepository, -} from "../../database/repositories/factory.js"; - -export type LiveProtocol = "ssh" | "rdp" | "vnc" | "telnet"; - -export async function isSharingEnabledForHost(hostId: number): Promise<{ - enabled: boolean; - hostOwnerId: string | null; -}> { - const globalEnabled = await createCurrentSettingsRepository().getBoolean( - "session_sharing_globally_enabled", - true, - ); - if (!globalEnabled) return { enabled: false, hostOwnerId: null }; - - const hostResolutionRepository = createCurrentHostResolutionRepository(); - const hostOwnerId = await hostResolutionRepository.findHostOwnerId(hostId); - if (!hostOwnerId) return { enabled: false, hostOwnerId: null }; - - const host = await hostResolutionRepository.findHostById(hostId, hostOwnerId); - if (!host) return { enabled: false, hostOwnerId: null }; - - return { - enabled: host.allowSessionSharing !== false, - hostOwnerId, - }; -} - -export function isLiveSessionOwnedBy( - protocol: LiveProtocol, - sessionId: string, - userId: string, -): boolean { - if (protocol === "ssh") { - const session = liveTerminalSessions.getSession(sessionId); - return !!session && session.isConnected && session.userId === userId; - } - const info = getGuacSessionInfo(sessionId); - return !!info && info.ownerUserId === userId; -} - -export function isLiveSession( - protocol: LiveProtocol, - sessionId: string, -): boolean { - if (protocol === "ssh") { - const session = liveTerminalSessions.getSession(sessionId); - return !!session && session.isConnected; - } - return !!getGuacSessionInfo(sessionId); -} diff --git a/src/backend/hosts/session-sharing/routes.ts b/src/backend/hosts/session-sharing/routes.ts deleted file mode 100644 index 028e9dd90..000000000 --- a/src/backend/hosts/session-sharing/routes.ts +++ /dev/null @@ -1,503 +0,0 @@ -import crypto from "crypto"; -import express, { type Request, type Response } from "express"; -import type { AuthenticatedRequest } from "../../../types/index.js"; -import { AuthManager } from "../../utils/auth-manager.js"; -import { PermissionManager } from "../../utils/permission-manager.js"; -import { sshLogger } from "../../utils/logger.js"; -import { liveTerminalSessions } from "../live-terminal-sessions.js"; -import { - isLiveSession, - isLiveSessionOwnedBy, - isSharingEnabledForHost, -} from "./live-sessions.js"; -// Remote Desktop is a first-party plugin (plugins/remote-desktop); this goes -// through the same in-core bridge its router is mounted behind, since -// plugins/ is compiled separately from src/backend/ and core cannot -// statically import across that boundary. See -// src/backend/database/routes/guacamole-dispatch.ts and that plugin's -// README -- this is an accepted, temporary coupling until session-sharing -// becomes a plugin of its own. -import { createGuacamoleJoinToken } from "../guacamole-sessions.js"; -import { createCurrentSessionShareRepository } from "../../database/repositories/factory.js"; - -const router = express.Router(); -const authManager = AuthManager.getInstance(); -const authenticateJWT = authManager.createAuthMiddleware(); -const permissionManager = PermissionManager.getInstance(); - -const DEFAULT_EXPIRY_HOURS = 24; -const MAX_EXPIRY_HOURS = 24 * 30; - -type Protocol = "ssh" | "rdp" | "vnc" | "telnet"; -type PermissionLevel = "read-only" | "read-write"; - -interface ResolveRateEntry { - count: number; - windowStart: number; -} -const resolveAttempts = new Map(); -function computeExpiresAt(expiryHours: number | undefined): string { - const hours = Math.min( - Math.max(expiryHours ?? DEFAULT_EXPIRY_HOURS, 1), - MAX_EXPIRY_HOURS, - ); - return new Date(Date.now() + hours * 60 * 60 * 1000).toISOString(); -} - -const RESOLVE_WINDOW_MS = 60 * 1000; -const RESOLVE_MAX_ATTEMPTS = 30; - -function isResolveRateLimited(ip: string): boolean { - const now = Date.now(); - const entry = resolveAttempts.get(ip); - if (!entry || now - entry.windowStart > RESOLVE_WINDOW_MS) { - resolveAttempts.set(ip, { count: 1, windowStart: now }); - return false; - } - entry.count += 1; - return entry.count > RESOLVE_MAX_ATTEMPTS; -} - -setInterval( - () => { - const now = Date.now(); - for (const [ip, entry] of resolveAttempts.entries()) { - if (now - entry.windowStart > RESOLVE_WINDOW_MS) - resolveAttempts.delete(ip); - } - }, - 5 * 60 * 1000, -).unref(); - -/** - * @openapi - * /session-sharing/create: - * post: - * summary: Create a session share (link or targeted user) - * description: Mints a share grant for a live terminal/RDP/VNC/Telnet session. Caller must own the live session. - * tags: - * - Session Sharing - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - hostId - * - sessionId - * - protocol - * - shareType - * - permissionLevel - * properties: - * hostId: - * type: integer - * sessionId: - * type: string - * tabInstanceId: - * type: string - * protocol: - * type: string - * enum: [ssh, rdp, vnc, telnet] - * shareType: - * type: string - * enum: [link, user] - * targetUserId: - * type: string - * permissionLevel: - * type: string - * enum: [read-only, read-write] - * expiryHours: - * type: number - * responses: - * 200: - * description: Share created - * 400: - * description: Invalid request - * 403: - * description: Sharing disabled, or caller does not own the session - * 500: - * description: Server error - */ -router.post("/create", authenticateJWT, async (req: Request, res: Response) => { - try { - const userId = (req as AuthenticatedRequest).userId!; - const { - hostId, - sessionId, - tabInstanceId, - protocol, - shareType, - targetUserId, - permissionLevel, - expiryHours, - } = req.body ?? {}; - - if (!hostId || !sessionId || !protocol || !shareType || !permissionLevel) { - return res.status(400).json({ error: "Missing required fields" }); - } - if (!["ssh", "rdp", "vnc", "telnet"].includes(protocol)) { - return res.status(400).json({ error: "Invalid protocol" }); - } - if (!["link", "user"].includes(shareType)) { - return res.status(400).json({ error: "Invalid shareType" }); - } - if (!["read-only", "read-write"].includes(permissionLevel)) { - return res.status(400).json({ error: "Invalid permissionLevel" }); - } - if (shareType === "user" && !targetUserId) { - return res - .status(400) - .json({ error: "targetUserId is required for user shares" }); - } - - const numericHostId = Number(hostId); - - const { enabled: sharingEnabled } = - await isSharingEnabledForHost(numericHostId); - if (!sharingEnabled) { - return res - .status(403) - .json({ error: "Session sharing is disabled for this host" }); - } - - if (!isLiveSessionOwnedBy(protocol, String(sessionId), userId)) { - return res - .status(403) - .json({ error: "You do not own this live session" }); - } - - if (shareType === "user") { - const accessInfo = await permissionManager.canAccessHost( - targetUserId, - numericHostId, - "connect", - ); - if (!accessInfo.hasAccess) { - return res.status(403).json({ - error: "Target user does not have access to this host", - }); - } - } - - const shareId = crypto.randomUUID(); - const linkToken = - shareType === "link" - ? crypto.randomBytes(24).toString("base64url") - : null; - const expiresAt = computeExpiresAt(expiryHours); - - const created = await createCurrentSessionShareRepository().create({ - id: shareId, - hostId: numericHostId, - ownerUserId: userId, - protocol, - sessionId: String(sessionId), - tabInstanceId: tabInstanceId ?? null, - shareType, - targetUserId: shareType === "user" ? targetUserId : null, - linkToken, - permissionLevel, - expiresAt, - }); - - res.json({ - shareId: created.id, - linkToken: created.linkToken, - expiresAt: created.expiresAt, - }); - } catch (error) { - sshLogger.error("Failed to create session share", error, { - operation: "session_share_create_error", - }); - res.status(500).json({ error: "Failed to create session share" }); - } -}); - -/** - * @openapi - * /session-sharing/host/{hostId}/active: - * get: - * summary: List active session shares for a host - * description: Returns active (non-revoked, non-expired) shares owned by the caller for the given host. - * tags: - * - Session Sharing - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: hostId - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: List of active shares - * 400: - * description: Invalid host id - * 500: - * description: Server error - */ -router.get( - "/host/:hostId/active", - authenticateJWT, - async (req: Request, res: Response) => { - try { - const userId = (req as AuthenticatedRequest).userId!; - const hostId = Number.parseInt(String(req.params.hostId), 10); - if (!hostId || Number.isNaN(hostId)) { - return res.status(400).json({ error: "Invalid host ID" }); - } - - const shares = - await createCurrentSessionShareRepository().findActiveSharesForHost( - hostId, - userId, - ); - - res.json({ shares }); - } catch (error) { - sshLogger.error("Failed to list session shares", error, { - operation: "session_share_list_error", - }); - res.status(500).json({ error: "Failed to list session shares" }); - } - }, -); - -/** - * @openapi - * /session-sharing/{shareId}: - * delete: - * summary: Revoke a session share - * description: Revokes a share. Owner or admin only. Best-effort kick of live SSH participants; guac joins are not force-disconnected in v1. - * tags: - * - Session Sharing - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: shareId - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Share revoked - * 403: - * description: Not authorized to revoke this share - * 404: - * description: Share not found - * 500: - * description: Server error - */ -router.delete( - "/:shareId", - authenticateJWT, - async (req: Request, res: Response) => { - try { - const userId = (req as AuthenticatedRequest).userId!; - const shareId = String(req.params.shareId); - - const repository = createCurrentSessionShareRepository(); - const share = await repository.findById(shareId); - if (!share) { - return res.status(404).json({ error: "Share not found" }); - } - - let revoked = await repository.revoke(shareId, userId); - if (!revoked) { - if (await permissionManager.isAdmin(userId)) { - revoked = await repository.revokeAsAdmin(shareId); - } - } - - if (!revoked) { - return res - .status(403) - .json({ error: "Not authorized to revoke this share" }); - } - - // Best-effort kick of live participants. SSH sessions support ending - // just the guests via ownerEndSession; guac joins aren't force-kickable - // from a REST handler (guacamole-lite exposes no kick API), so a revoked - // guac link only blocks *future* resolves until the guest's own socket ends. - if (share.protocol === "ssh") { - try { - liveTerminalSessions.ownerEndSession( - share.sessionId, - "Session share revoked by owner", - ); - } catch { - // best-effort only - } - } - - res.json({ success: true }); - } catch (error) { - sshLogger.error("Failed to revoke session share", error, { - operation: "session_share_revoke_error", - }); - res.status(500).json({ error: "Failed to revoke session share" }); - } - }, -); - -/** - * @openapi - * /session-sharing/resolve/{linkToken}: - * get: - * summary: Resolve a guest share link - * description: Public, unauthenticated endpoint for anonymous share-link guests. Never returns host name, IP, username, or hostId. Rate-limited per IP. - * tags: - * - Session Sharing - * parameters: - * - in: path - * name: linkToken - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Resolved share connection info - * 404: - * description: Link not found, expired, revoked, or sharing disabled - * 429: - * description: Too many requests - * 500: - * description: Server error - */ -router.get("/resolve/:linkToken", async (req: Request, res: Response) => { - try { - const ip = req.ip || req.socket.remoteAddress || "unknown"; - if (isResolveRateLimited(ip)) { - return res.status(429).json({ error: "Too many requests" }); - } - - const linkToken = String(req.params.linkToken); - const repository = createCurrentSessionShareRepository(); - const share = await repository.findByLinkToken(linkToken); - if (!share) { - return res.status(404).json({ error: "Link not found or expired" }); - } - - const { enabled: sharingEnabled } = await isSharingEnabledForHost( - share.hostId, - ); - if (!sharingEnabled) { - return res.status(404).json({ error: "Link not found or expired" }); - } - - const protocol = share.protocol as Protocol; - if (!isLiveSession(protocol, share.sessionId)) { - return res.status(404).json({ error: "Session is no longer active" }); - } - - // Field-by-field by design - never spread a host row into this response. - // Anonymous guests must never see hostname/IP/username/hostId (decision #5). - const response: { - protocol: Protocol; - permissionLevel: PermissionLevel; - wsPath: string; - connectParams?: Record; - } = { - protocol, - permissionLevel: share.permissionLevel as PermissionLevel, - // Advisory only: the client builds its own socket URL per deployment - // (see pluginWsUrl). Kept accurate rather than left pointing at the - // ports these two moved off in A4. - wsPath: - protocol === "ssh" - ? `/plugin-ws/ssh-terminal/terminal?shareToken=${encodeURIComponent(linkToken)}` - : "/plugin-ws/remote-desktop/display", - }; - - if (protocol !== "ssh") { - const joinToken = createGuacamoleJoinToken( - share.sessionId, - share.permissionLevel === "read-only", - ); - response.connectParams = { token: joinToken }; - } - - try { - await repository.touchShareUsage(share.id); - await repository.recordParticipantJoin(share.id, null, "Guest"); - } catch { - // best-effort, never fail the resolve response over audit bookkeeping - } - - res.json(response); - } catch (error) { - sshLogger.error("Failed to resolve session share link", error, { - operation: "session_share_resolve_error", - }); - res.status(500).json({ error: "Failed to resolve share link" }); - } -}); - -/** - * @openapi - * /session-sharing/{shareId}/end: - * post: - * summary: End a shared session for all participants - * description: Owner-only. Terminates the underlying session and notifies joined participants. Guac protocol kick is best-effort in v1. - * tags: - * - Session Sharing - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: shareId - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Session ended - * 403: - * description: Not the owner of this share - * 404: - * description: Share not found - * 500: - * description: Server error - */ -router.post( - "/:shareId/end", - authenticateJWT, - async (req: Request, res: Response) => { - try { - const userId = (req as AuthenticatedRequest).userId!; - const shareId = String(req.params.shareId); - - const repository = createCurrentSessionShareRepository(); - const share = await repository.findById(shareId); - if (!share) { - return res.status(404).json({ error: "Share not found" }); - } - if (share.ownerUserId !== userId) { - return res.status(403).json({ error: "Not the owner of this share" }); - } - - if (share.protocol === "ssh") { - liveTerminalSessions.ownerEndSession( - share.sessionId, - "Session ended by owner", - ); - } - // Guac protocols: no kick API available from a REST handler in v1 - see - // DELETE /:shareId for the same limitation. - - res.json({ success: true }); - } catch (error) { - sshLogger.error("Failed to end shared session", error, { - operation: "session_share_end_error", - }); - res.status(500).json({ error: "Failed to end shared session" }); - } - }, -); - -export default router; diff --git a/src/backend/plugins/ctx.ts b/src/backend/plugins/ctx.ts index 4abd96151..5568a867f 100644 --- a/src/backend/plugins/ctx.ts +++ b/src/backend/plugins/ctx.ts @@ -435,7 +435,7 @@ export function createPluginContext( }, services: { - provide: (service, implementation) => { + provide: (service, implementation, options) => { // Declared AND provided: a plugin cannot publish a service its // manifest never mentioned. const entry = manifest.provides?.find( @@ -446,9 +446,16 @@ export function createPluginContext( `Plugin ${pluginId} cannot provide service "${service}": it is not declared in the manifest's provides array`, ); } + const name = options?.name ?? ""; + if (entry.names ? !entry.names.includes(name) : name !== "") { + throw new Error( + `Plugin ${pluginId} cannot provide service "${service}" as "${name}": the manifest's provides[].names does not list it`, + ); + } const registration = serviceRegistry.provideService({ service, + name, version: entry.version, permission: entry.permission, pluginId, @@ -463,14 +470,21 @@ export function createPluginContext( }, get: (service, options) => - serviceRegistry.createServiceHandle(service, pluginId, { - // A caller-supplied userId is only honoured when it matches the - // actor; otherwise the ambient actor wins. A plugin cannot widen - // its reach by naming someone else here. - resolveUserId: () => options?.userId ?? getActor(), - hasPermission: checkPermission, - audit: (entry) => writeServiceAudit(entry), - }), + serviceRegistry.createServiceHandle( + service, + pluginId, + { + // A caller-supplied userId is only honoured when it matches the + // actor; otherwise the ambient actor wins. A plugin cannot widen + // its reach by naming someone else here. + resolveUserId: () => options?.userId ?? getActor(), + hasPermission: checkPermission, + audit: (entry) => writeServiceAudit(entry), + }, + options?.provider ?? "", + ), + + providers: (service) => serviceRegistry.listProviderNames(service), }, secrets: { diff --git a/src/backend/plugins/registry.ts b/src/backend/plugins/registry.ts index 703129e61..fc5f65840 100644 --- a/src/backend/plugins/registry.ts +++ b/src/backend/plugins/registry.ts @@ -7,8 +7,8 @@ * - core registers the SSH connection pool as "ssh.transport" at boot, so * everything that needs a pooled connection resolves a provider rather * than importing the pool directly. - * - the ssh-terminal plugin publishes "terminal.sessions" so collab and - * session-sharing can reach live sessions without importing a plugin. + * - the session-sharing plugin publishes "sessions.sharing.guests", guest + * link resolution for the terminal, keyed by a token rather than a user. * * Deliberately NOT reachable from a worker plugin's ctx. Values here are live * objects -- pools, session managers, sockets -- and handing one across the diff --git a/src/backend/plugins/service-registry.ts b/src/backend/plugins/service-registry.ts index ea6b96fe2..a8eb9497d 100644 --- a/src/backend/plugins/service-registry.ts +++ b/src/backend/plugins/service-registry.ts @@ -42,6 +42,11 @@ import type { PluginManifest, PluginServiceRequire } from "./manifest.js"; export interface ServiceRegistration { service: string; + /** + * The provider's name, for a service several plugins provide side by side + * (sessions.live, keyed by session type). Empty for the one unnamed provider. + */ + name: string; version: string; permission: string; /** The plugin that provided it. */ @@ -57,9 +62,10 @@ export interface ServiceRegistration { generation: number; } -const services = new Map(); +/** service -> provider name ("" when unnamed) -> registration. */ +const services = new Map>(); /** - * What a service looked like before it was revoked. + * What a provider looked like before it was revoked, by the same two keys. * * A handle taken while the provider was up must keep throwing a named error * after it goes away, rather than degrading to "undefined is not a function", @@ -68,6 +74,14 @@ const services = new Map(); const retired = new Map }>(); let generationCounter = 0; +function retiredKey(service: string, name: string): string { + return `${service}#${name}`; +} + +function lookup(service: string, name = ""): ServiceRegistration | undefined { + return services.get(service)?.get(name); +} + /** Thrown when the provider went away while a consumer still held a handle. */ export class PluginServiceUnavailableError extends Error { readonly code = "EPLUGINSVCGONE"; @@ -115,22 +129,31 @@ export class PluginServiceActorError extends Error { } export function provideService( - registration: Omit, + registration: Omit & { + name?: string; + }, ): ServiceRegistration { - const existing = services.get(registration.service); + const name = registration.name ?? ""; + const existing = lookup(registration.service, name); if (existing) { pluginLogger.warn( - `Service "${registration.service}" is already provided by ${existing.pluginId} and is being replaced by ${registration.pluginId}`, + `Service "${registration.service}"${name ? ` (${name})` : ""} is already provided by ${existing.pluginId} and is being replaced by ${registration.pluginId}`, { operation: "plugin_service_registry" }, ); } const entry: ServiceRegistration = { ...registration, + name, generation: ++generationCounter, }; - services.set(registration.service, entry); - retired.delete(registration.service); + let providers = services.get(registration.service); + if (!providers) { + providers = new Map(); + services.set(registration.service, providers); + } + providers.set(name, entry); + retired.delete(retiredKey(registration.service, name)); return entry; } @@ -143,13 +166,14 @@ export function revokeService( service: string, registration?: ServiceRegistration, ): boolean { - const current = services.get(service); + const name = registration?.name ?? ""; + const current = lookup(service, name); if (!current) return false; if (registration && current.generation !== registration.generation) { return false; } - retired.set(service, { + retired.set(retiredKey(service, name), { pluginId: current.pluginId, methods: new Set( Object.keys(current.implementation).filter( @@ -157,13 +181,22 @@ export function revokeService( ), ), }); - return services.delete(service); + const providers = services.get(service)!; + providers.delete(name); + if (providers.size === 0) services.delete(service); + return true; } export function getRegistration( service: string, + name = "", ): ServiceRegistration | undefined { - return services.get(service); + return lookup(service, name); +} + +/** The names of every provider of a service right now ("" when unnamed). */ +export function listProviderNames(service: string): string[] { + return [...(services.get(service)?.keys() ?? [])]; } /** @@ -178,15 +211,16 @@ export function getRegistration( export function getServiceImplementation( service: string, versionRange: string, + name = "", ): T | undefined { - const registration = services.get(service); + const registration = lookup(service, name); if (!registration) return undefined; if (!semver.satisfies(registration.version, versionRange)) return undefined; return registration.implementation as T; } export function listServices(): ServiceRegistration[] { - return [...services.values()]; + return [...services.values()].flatMap((providers) => [...providers.values()]); } /** Test seam. */ @@ -214,7 +248,13 @@ export function resolveRequirements( const missingOptional: string[] = []; for (const requirement of manifest.requires ?? []) { - const registration = services.get(requirement.service); + // Any one provider satisfies it; a consumer of a named service picks the + // provider per call. + const providers = [...(services.get(requirement.service)?.values() ?? [])]; + const registration = + providers.find((candidate) => + semver.satisfies(candidate.version, requirement.versionRange), + ) ?? providers[0]; if (!registration) { recordUnsatisfied( @@ -292,6 +332,7 @@ export function createServiceHandle( service: string, consumerPluginId: string, context: ServiceCallContext, + name = "", ): T { const target = Object.create(null) as T; @@ -301,10 +342,12 @@ export function createServiceHandle( if (property === "asUser") { return (userId: string) => - createServiceHandle(service, consumerPluginId, { - ...context, - resolveUserId: () => userId, - }); + createServiceHandle( + service, + consumerPluginId, + { ...context, resolveUserId: () => userId }, + name, + ); } // Resolved at call time, not here: the provider may have gone away @@ -312,9 +355,9 @@ export function createServiceHandle( // must keep throwing the typed error rather than degrading to // `undefined is not a function`, which tells a consumer nothing about // why its dependency vanished. - const registration = services.get(service); + const registration = lookup(service, name); if (!registration) { - const gone = retired.get(service); + const gone = retired.get(retiredKey(service, name)); if (!gone?.methods.has(property)) return undefined; return () => { throw new PluginServiceUnavailableError(service, gone.pluginId); @@ -332,17 +375,18 @@ export function createServiceHandle( consumerPluginId, context, registration.generation, + name, ); }, has(_target, property) { - const registration = services.get(service); + const registration = lookup(service, name); if (!registration || typeof property !== "string") return false; return typeof registration.implementation[property] === "function"; }, ownKeys() { - const registration = services.get(service); + const registration = lookup(service, name); if (!registration) return []; return Object.keys(registration.implementation).filter( (key) => typeof registration.implementation[key] === "function", @@ -350,7 +394,7 @@ export function createServiceHandle( }, getOwnPropertyDescriptor(_target, property) { - const registration = services.get(service); + const registration = lookup(service, name); if (!registration || typeof property !== "string") return undefined; if (typeof registration.implementation[property] !== "function") { return undefined; @@ -367,8 +411,9 @@ async function invokeGuarded( consumerPluginId: string, context: ServiceCallContext, expectedGeneration: number, + name: string, ): Promise { - const registration = services.get(service); + const registration = lookup(service, name); // Re-checked here rather than trusted from the get trap: a call is async and // the provider can be disabled between resolving the method and running it. diff --git a/src/backend/starter.ts b/src/backend/starter.ts index d709d5037..1782af8bb 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -369,6 +369,10 @@ async function provisionLocalDesktopUserIfNeeded(): Promise { const { runTmuxMonitorSettingsMigration } = await import("./utils/crypto-migration/tmux-monitor-settings-migration.js"); await runTmuxMonitorSettingsMigration(); + + const { runSessionSharingSettingsMigration } = + await import("./utils/crypto-migration/session-sharing-settings-migration.js"); + await runSessionSharingSettingsMigration(); } catch (error) { systemLogger.warn("Plugin runtime failed to initialize", { operation: "plugin_init", diff --git a/src/backend/tests/database/db/bootstrap-covers-all-tables.test.ts b/src/backend/tests/database/db/bootstrap-covers-all-tables.test.ts index 6d1b41f5f..9760bc842 100644 --- a/src/backend/tests/database/db/bootstrap-covers-all-tables.test.ts +++ b/src/backend/tests/database/db/bootstrap-covers-all-tables.test.ts @@ -29,8 +29,6 @@ describe("bootstrap creates the tables the removed probes covered", () => { "audit_logs", "session_recordings", "api_keys", - "session_shares", - "session_share_participants", ]; beforeEach(() => { diff --git a/src/backend/tests/database/repositories/session-share-repository.test.ts b/src/backend/tests/database/repositories/session-share-repository.test.ts deleted file mode 100644 index 9f8e16cc1..000000000 --- a/src/backend/tests/database/repositories/session-share-repository.test.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { TestSqliteDatabase } from "./test-support.js"; -import { SessionShareRepository } from "../../../database/repositories/session-share-repository.js"; - -describe("SessionShareRepository", () => { - let adapter: TestSqliteDatabase | null = null; - - afterEach(async () => { - if (adapter) { - await adapter.close(); - adapter = null; - } - }); - - async function createRepository( - onWrite?: () => void | Promise, - ): Promise { - adapter = new TestSqliteDatabase(); - const context = await adapter.connect(); - await adapter.exec(` - INSERT INTO users (id, username, password_hash) - VALUES ('owner-1', 'alice', 'hash'), ('guest-1', 'bob', 'hash'); - INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) - VALUES (1, 'owner-1', 'host-one', '10.0.0.1', 22, 'root', 'password'), (2, 'owner-1', 'host-two', '10.0.0.2', 22, 'root', 'password'); - `); - - return new SessionShareRepository(context, onWrite); - } - - const FAR_FUTURE = "2999-01-01T00:00:00.000Z"; - const FAR_PAST = "2000-01-01T00:00:00.000Z"; - - it("creates a share and finds it by id", async () => { - const repo = await createRepository(); - - const created = await repo.create({ - id: "share-1", - hostId: 1, - ownerUserId: "owner-1", - protocol: "ssh", - sessionId: "session-1", - shareType: "link", - linkToken: "token-abc", - permissionLevel: "read-only", - expiresAt: FAR_FUTURE, - }); - - expect(created).toMatchObject({ - id: "share-1", - hostId: 1, - ownerUserId: "owner-1", - protocol: "ssh", - linkToken: "token-abc", - permissionLevel: "read-only", - }); - - const found = await repo.findById("share-1"); - expect(found).toMatchObject({ id: "share-1", sessionId: "session-1" }); - }); - - it("findByLinkToken excludes revoked shares", async () => { - const repo = await createRepository(); - await repo.create({ - id: "share-revoked", - hostId: 1, - ownerUserId: "owner-1", - protocol: "ssh", - sessionId: "session-1", - shareType: "link", - linkToken: "token-revoked", - permissionLevel: "read-only", - expiresAt: FAR_FUTURE, - }); - - expect(await repo.findByLinkToken("token-revoked")).not.toBeNull(); - - await repo.revoke("share-revoked", "owner-1"); - - expect(await repo.findByLinkToken("token-revoked")).toBeNull(); - }); - - it("findByLinkToken excludes expired shares", async () => { - const repo = await createRepository(); - await repo.create({ - id: "share-expired", - hostId: 1, - ownerUserId: "owner-1", - protocol: "ssh", - sessionId: "session-1", - shareType: "link", - linkToken: "token-expired", - permissionLevel: "read-only", - expiresAt: FAR_PAST, - }); - - expect(await repo.findByLinkToken("token-expired")).toBeNull(); - }); - - it("findByLinkToken returns active, non-expired, non-revoked shares", async () => { - const repo = await createRepository(); - await repo.create({ - id: "share-active", - hostId: 1, - ownerUserId: "owner-1", - protocol: "vnc", - sessionId: "guac-session-1", - shareType: "link", - linkToken: "token-active", - permissionLevel: "read-write", - expiresAt: FAR_FUTURE, - }); - - const found = await repo.findByLinkToken("token-active"); - expect(found).toMatchObject({ - id: "share-active", - protocol: "vnc", - permissionLevel: "read-write", - }); - }); - - it("findSharesTargetingUser returns only active user-targeted shares with host/owner metadata", async () => { - const repo = await createRepository(); - - await repo.create({ - id: "share-user-active", - hostId: 1, - ownerUserId: "owner-1", - protocol: "ssh", - sessionId: "session-1", - shareType: "user", - targetUserId: "guest-1", - permissionLevel: "read-write", - expiresAt: FAR_FUTURE, - }); - - // Expired user share for the same target - must be excluded - await repo.create({ - id: "share-user-expired", - hostId: 2, - ownerUserId: "owner-1", - protocol: "ssh", - sessionId: "session-2", - shareType: "user", - targetUserId: "guest-1", - permissionLevel: "read-only", - expiresAt: FAR_PAST, - }); - - // Link share, not targeting a user - must be excluded even though it's active - await repo.create({ - id: "share-link-active", - hostId: 1, - ownerUserId: "owner-1", - protocol: "ssh", - sessionId: "session-3", - shareType: "link", - linkToken: "token-unrelated", - permissionLevel: "read-only", - expiresAt: FAR_FUTURE, - }); - - const shares = await repo.findSharesTargetingUser("guest-1"); - expect(shares).toHaveLength(1); - expect(shares[0]).toMatchObject({ - id: "share-user-active", - hostName: "host-one", - ownerUsername: "alice", - }); - }); - - it("revoke only affects the requesting owner's own share", async () => { - const repo = await createRepository(); - await repo.create({ - id: "share-owned", - hostId: 1, - ownerUserId: "owner-1", - protocol: "ssh", - sessionId: "session-1", - shareType: "link", - linkToken: "token-owned", - permissionLevel: "read-only", - expiresAt: FAR_FUTURE, - }); - - expect(await repo.revoke("share-owned", "guest-1")).toBe(false); - expect(await repo.revoke("share-owned", "owner-1")).toBe(true); - }); - - it("revokeAsAdmin revokes regardless of owner", async () => { - const repo = await createRepository(); - await repo.create({ - id: "share-admin-target", - hostId: 1, - ownerUserId: "owner-1", - protocol: "ssh", - sessionId: "session-1", - shareType: "link", - linkToken: "token-admin", - permissionLevel: "read-only", - expiresAt: FAR_FUTURE, - }); - - expect(await repo.revokeAsAdmin("share-admin-target")).toBe(true); - expect(await repo.findByLinkToken("token-admin")).toBeNull(); - }); - - it("deleteExpiredShares removes only expired rows", async () => { - const repo = await createRepository(); - await repo.create({ - id: "share-old", - hostId: 1, - ownerUserId: "owner-1", - protocol: "ssh", - sessionId: "session-1", - shareType: "link", - linkToken: "token-old", - permissionLevel: "read-only", - expiresAt: FAR_PAST, - }); - await repo.create({ - id: "share-current", - hostId: 1, - ownerUserId: "owner-1", - protocol: "ssh", - sessionId: "session-2", - shareType: "link", - linkToken: "token-current", - permissionLevel: "read-only", - expiresAt: FAR_FUTURE, - }); - - const deletedCount = await repo.deleteExpiredShares(); - expect(deletedCount).toBe(1); - expect(await repo.findById("share-old")).toBeNull(); - expect(await repo.findById("share-current")).not.toBeNull(); - }); - - it("touchShareUsage increments joinCount and sets lastJoinedAt", async () => { - const repo = await createRepository(); - await repo.create({ - id: "share-touch", - hostId: 1, - ownerUserId: "owner-1", - protocol: "ssh", - sessionId: "session-1", - shareType: "link", - linkToken: "token-touch", - permissionLevel: "read-only", - expiresAt: FAR_FUTURE, - }); - - await repo.touchShareUsage("share-touch", "2026-01-01T00:00:00.000Z"); - let row = await repo.findById("share-touch"); - expect(row?.joinCount).toBe(1); - expect(row?.lastJoinedAt).toBe("2026-01-01T00:00:00.000Z"); - - await repo.touchShareUsage("share-touch", "2026-01-02T00:00:00.000Z"); - row = await repo.findById("share-touch"); - expect(row?.joinCount).toBe(2); - }); - - it("records and closes participant joins", async () => { - const repo = await createRepository(); - await repo.create({ - id: "share-participants", - hostId: 1, - ownerUserId: "owner-1", - protocol: "ssh", - sessionId: "session-1", - shareType: "link", - linkToken: "token-participants", - permissionLevel: "read-write", - expiresAt: FAR_FUTURE, - }); - - const participant = await repo.recordParticipantJoin( - "share-participants", - null, - "Guest", - ); - expect(participant).toMatchObject({ - shareId: "share-participants", - userId: null, - guestLabel: "Guest", - }); - expect(participant.leftAt).toBeNull(); - - await repo.recordParticipantLeave(participant.id); - }); - - it("write hook fires on mutating operations", async () => { - let writeCount = 0; - const repo = await createRepository(() => { - writeCount += 1; - }); - - await repo.create({ - id: "share-write-hook", - hostId: 1, - ownerUserId: "owner-1", - protocol: "ssh", - sessionId: "session-1", - shareType: "link", - linkToken: "token-write-hook", - permissionLevel: "read-only", - expiresAt: FAR_FUTURE, - }); - expect(writeCount).toBe(1); - - await repo.revoke("share-write-hook", "owner-1"); - expect(writeCount).toBe(2); - }); - - it("deleteSharesForHost removes all shares for a host", async () => { - const repo = await createRepository(); - await repo.create({ - id: "share-host-1a", - hostId: 1, - ownerUserId: "owner-1", - protocol: "ssh", - sessionId: "session-1", - shareType: "link", - linkToken: "token-h1a", - permissionLevel: "read-only", - expiresAt: FAR_FUTURE, - }); - await repo.create({ - id: "share-host-1b", - hostId: 1, - ownerUserId: "owner-1", - protocol: "ssh", - sessionId: "session-2", - shareType: "link", - linkToken: "token-h1b", - permissionLevel: "read-only", - expiresAt: FAR_FUTURE, - }); - await repo.create({ - id: "share-host-2", - hostId: 2, - ownerUserId: "owner-1", - protocol: "ssh", - sessionId: "session-3", - shareType: "link", - linkToken: "token-h2", - permissionLevel: "read-only", - expiresAt: FAR_FUTURE, - }); - - expect(await repo.deleteSharesForHost(1)).toBe(2); - expect(await repo.findById("share-host-2")).not.toBeNull(); - }); -}); diff --git a/src/backend/tests/database/routes/plugins.test.ts b/src/backend/tests/database/routes/plugins.test.ts index 348e5c0b7..7d9b419e6 100644 --- a/src/backend/tests/database/routes/plugins.test.ts +++ b/src/backend/tests/database/routes/plugins.test.ts @@ -204,7 +204,7 @@ describe("plugins route", () => { id: "guest-plugin", manifestJson: JSON.stringify({ capabilities: ["hosts:read"], - contributes: { guest: true }, + contributes: { guest: true, guestViews: ["shared"] }, }), }), ); @@ -225,7 +225,10 @@ describe("plugins route", () => { expect(body.map((plugin: { id: string }) => plugin.id)).toEqual([ "guest-plugin", ]); - expect(body[0].contributes).toEqual({ guest: true }); + expect(body[0].contributes).toEqual({ + guest: true, + guestViews: ["shared"], + }); expect(body[0]).not.toHaveProperty("capabilities"); expect(body[0]).not.toHaveProperty("lastError"); }); diff --git a/src/backend/tests/hosts/collab/room-hub.test.ts b/src/backend/tests/hosts/collab/room-hub.test.ts deleted file mode 100644 index 29fb49aa7..000000000 --- a/src/backend/tests/hosts/collab/room-hub.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import type { WebSocket } from "ws"; - -const runtime = vi.hoisted(() => ({ - listener: null as ((roomId: string, message: object) => void) | null, - publish: vi.fn(async () => undefined), - updatePresence: vi.fn(async () => undefined), -})); - -vi.mock("../../../hosts/collab/runtime-store.js", () => ({ - collabRuntimeStore: { - onEvent: (listener: (roomId: string, message: object) => void) => { - runtime.listener = listener; - }, - publish: runtime.publish, - updatePresence: runtime.updatePresence, - onlineUsers: async ( - _roomId: string, - users: Array<{ userId: string; username: string }>, - ) => users, - }, -})); - -import { collabRoomHub } from "../../../hosts/collab/room-hub.js"; - -function fakeWs(open = true): WebSocket { - return { - OPEN: 1, - readyState: open ? 1 : 3, - send: vi.fn(), - } as unknown as WebSocket; -} - -describe("collabRoomHub", () => { - it("announces the online list on subscribe and unsubscribe, deduplicated per user", async () => { - const a1 = fakeWs(); - const a2 = fakeWs(); - const b = fakeWs(); - collabRoomHub.subscribe("room-1", { ws: a1, userId: "a", username: "A" }); - collabRoomHub.subscribe("room-1", { ws: a2, userId: "a", username: "A" }); - collabRoomHub.subscribe("room-1", { ws: b, userId: "b", username: "B" }); - - expect(await collabRoomHub.onlineUsers("room-1")).toEqual([ - { userId: "a", username: "A" }, - { userId: "b", username: "B" }, - ]); - - await vi.waitFor(() => expect(b.send).toHaveBeenCalled()); - const last = JSON.parse( - (b.send as ReturnType).mock.calls.at(-1)?.[0] as string, - ); - expect(last).toEqual({ - type: "collab_online", - roomId: "room-1", - users: [ - { userId: "a", username: "A" }, - { userId: "b", username: "B" }, - ], - }); - - collabRoomHub.unsubscribe(a1); - expect(await collabRoomHub.onlineUsers("room-1")).toHaveLength(2); - collabRoomHub.unsubscribe(a2); - expect(await collabRoomHub.onlineUsers("room-1")).toEqual([ - { userId: "b", username: "B" }, - ]); - collabRoomHub.unsubscribe(b); - expect(await collabRoomHub.onlineUsers("room-1")).toEqual([]); - }); - - it("subscribing the same socket twice keeps one subscription", async () => { - const ws = fakeWs(); - collabRoomHub.subscribe("room-2", { ws, userId: "a", username: "A" }); - collabRoomHub.subscribe("room-2", { ws, userId: "a", username: "A" }); - await vi.waitFor(() => - expect((ws.send as ReturnType).mock.calls).toHaveLength(1), - ); - collabRoomHub.unsubscribe(ws); - }); - - it("broadcast skips closed sockets and rooms nobody watches", () => { - const open = fakeWs(); - const closed = fakeWs(false); - collabRoomHub.subscribe("room-3", { ws: open, userId: "a", username: "A" }); - collabRoomHub.subscribe("room-3", { - ws: closed, - userId: "b", - username: "B", - }); - (open.send as ReturnType).mockClear(); - (closed.send as ReturnType).mockClear(); - - collabRoomHub.broadcast("room-3", { type: "collab_stage_changed" }); - collabRoomHub.broadcast("nobody", { type: "collab_stage_changed" }); - - expect(open.send).toHaveBeenCalledTimes(1); - expect(closed.send).not.toHaveBeenCalled(); - collabRoomHub.unsubscribe(open); - collabRoomHub.unsubscribe(closed); - }); - - it("fans out remote Redis events but keeps internal events server-side", () => { - const ws = fakeWs(); - collabRoomHub.subscribe("room-4", { ws, userId: "a", username: "A" }); - (ws.send as ReturnType).mockClear(); - - runtime.listener?.("room-4", { type: "collab_members_changed" }); - runtime.listener?.("room-4", { type: "collab_internal_stage_revoked" }); - - expect(ws.send).toHaveBeenCalledTimes(1); - collabRoomHub.unsubscribe(ws); - }); -}); diff --git a/src/backend/tests/hosts/collab/room-share-access.test.ts b/src/backend/tests/hosts/collab/room-share-access.test.ts deleted file mode 100644 index a51d29baf..000000000 --- a/src/backend/tests/hosts/collab/room-share-access.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const state = vi.hoisted(() => ({ - roomsByShare: new Map(), - members: new Set(), // `${roomId}:${userId}` -})); - -vi.mock("../../../database/repositories/factory.js", () => ({ - createCurrentCollabRoomRepository: () => ({ - findByStageShareId: async (shareId: string) => - state.roomsByShare.get(shareId) ?? null, - findMember: async (roomId: string, userId: string) => - state.members.has(`${roomId}:${userId}`) ? { roomId, userId } : null, - }), -})); - -const { canJoinRoomStageShare } = - await import("../../../hosts/collab/room-share-access.js"); - -describe("canJoinRoomStageShare", () => { - beforeEach(() => { - state.roomsByShare.clear(); - state.members.clear(); - }); - - it("admits members of the live room whose stage is this share", async () => { - state.roomsByShare.set("share-1", { id: "room-1" }); - state.members.add("room-1:alice"); - await expect(canJoinRoomStageShare("share-1", "alice")).resolves.toBe(true); - }); - - it("rejects non-members and shares that are not a room stage", async () => { - state.roomsByShare.set("share-1", { id: "room-1" }); - await expect(canJoinRoomStageShare("share-1", "mallory")).resolves.toBe( - false, - ); - await expect(canJoinRoomStageShare("share-9", "alice")).resolves.toBe( - false, - ); - }); -}); diff --git a/src/backend/tests/hosts/collab/routes.test.ts b/src/backend/tests/hosts/collab/routes.test.ts deleted file mode 100644 index a0adde1d0..000000000 --- a/src/backend/tests/hosts/collab/routes.test.ts +++ /dev/null @@ -1,689 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { Request, Response } from "express"; - -type Room = { - id: string; - name: string; - ownerUserId: string; - persistent: boolean; - presenterUserId: string | null; - stageProtocol: string | null; - stageHostId: number | null; - stageShareId: string | null; - guestLinkToken: string | null; - createdAt: string; - endedAt: string | null; -}; - -const state = vi.hoisted(() => ({ - currentUserId: "host-1", - rooms: new Map(), - members: new Map< - string, - { roomId: string; userId: string; roomRole: string } - >(), - shares: new Map>(), - users: new Set(["host-1", "alice", "bob"]), - roles: new Map([[7, ["alice", "carol"]]]), - sharingEnabled: true, - liveOwned: new Map(), // sessionId -> owner - broadcasts: [] as Array>, - control: [] as Array, - controllers: new Map(), - requests: new Map< - string, - Map - >(), -})); - -vi.mock("../../../utils/logger.js", () => ({ - sshLogger: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), success: vi.fn() }, -})); -vi.mock("../../../utils/auth-manager.js", () => ({ - AuthManager: { - getInstance: () => ({ - createAuthMiddleware: - () => - (req: Record, _res: unknown, next: () => void) => { - req.userId = state.currentUserId; - next(); - }, - }), - }, -})); -vi.mock("../../../utils/audit-logger.js", () => ({ - logAudit: vi.fn(async () => undefined), - getAuditUsername: vi.fn(async (id: string) => id.toUpperCase()), - getRequestMeta: () => ({ ipAddress: "127.0.0.1", userAgent: "test" }), -})); -vi.mock("../../../hosts/guacamole-sessions.js", () => ({ - createGuacamoleJoinToken: (id: string, readOnly: boolean) => - `join:${id}:${readOnly}`, -})); -vi.mock("../../../hosts/collab/room-hub.js", () => ({ - collabRoomHub: { - broadcast: (roomId: string, message: Record) => { - state.broadcasts.push({ roomId, ...message }); - }, - onlineUsers: () => [], - }, -})); -vi.mock("../../../hosts/collab/runtime-store.js", () => ({ - collabRuntimeStore: { - onEvent: vi.fn(), - publish: vi.fn(async () => undefined), - getController: async (roomId: string) => - state.controllers.get(roomId) ?? null, - setController: async (roomId: string, userId: string | null) => { - if (userId) state.controllers.set(roomId, userId); - else state.controllers.delete(roomId); - }, - listRequests: async (roomId: string) => - Array.from(state.requests.get(roomId)?.values() ?? []).sort((a, b) => - a.requestedAt.localeCompare(b.requestedAt), - ), - upsertRequest: async ( - roomId: string, - request: { userId: string; username: string; requestedAt: string }, - ) => { - let requests = state.requests.get(roomId); - if (!requests) { - requests = new Map(); - state.requests.set(roomId, requests); - } - requests.set(request.userId, request); - }, - removeRequest: async (roomId: string, userId: string) => { - state.requests.get(roomId)?.delete(userId); - }, - clearRequests: async (roomId: string) => { - state.requests.delete(roomId); - }, - }, -})); -vi.mock("../../../hosts/live-terminal-sessions.js", () => ({ - liveTerminalSessions: { - setRoomShareControl: (...args: unknown[]) => { - state.control.push(args); - }, - disconnectShareParticipants: vi.fn(() => 0), - }, -})); -vi.mock("../../../hosts/session-sharing/live-sessions.js", () => ({ - isSharingEnabledForHost: async () => ({ - enabled: state.sharingEnabled, - hostOwnerId: "host-1", - }), - isLiveSessionOwnedBy: (_p: string, sessionId: string, userId: string) => - state.liveOwned.get(sessionId) === userId, - isLiveSession: (_p: string, sessionId: string) => - state.liveOwned.has(sessionId), -})); - -vi.mock("../../../database/repositories/factory.js", () => ({ - createCurrentCollabRoomRepository: () => ({ - createRoom: async ( - input: Omit< - Room, - | "presenterUserId" - | "stageProtocol" - | "stageHostId" - | "stageShareId" - | "guestLinkToken" - | "createdAt" - | "endedAt" - >, - ) => { - const room: Room = { - ...input, - presenterUserId: null, - stageProtocol: null, - stageHostId: null, - stageShareId: null, - guestLinkToken: null, - createdAt: "2026-08-25T00:00:00.000Z", - endedAt: null, - }; - state.rooms.set(room.id, room); - return room; - }, - findById: async (id: string) => state.rooms.get(id) ?? null, - findByGuestToken: async (token: string) => - [...state.rooms.values()].find( - (r) => r.guestLinkToken === token && !r.endedAt, - ) ?? null, - setGuestToken: async (roomId: string, token: string | null) => { - state.rooms.get(roomId)!.guestLinkToken = token; - }, - listForUser: async (userId: string) => - [...state.members.values()] - .filter((m) => m.userId === userId) - .map((m) => state.rooms.get(m.roomId)!) - .filter((r) => !r.endedAt), - findMember: async (roomId: string, userId: string) => - state.members.get(`${roomId}:${userId}`) ?? null, - addMember: async (input: { - roomId: string; - userId: string; - roomRole: string; - }) => { - const key = `${input.roomId}:${input.userId}`; - if (state.members.has(key)) return false; - state.members.set(key, input); - return true; - }, - removeMember: async (roomId: string, userId: string) => { - state.members.delete(`${roomId}:${userId}`); - }, - listMembers: async (roomId: string) => - [...state.members.values()] - .filter((m) => m.roomId === roomId) - .map((m) => ({ ...m, username: m.userId, createdAt: "" })), - updateStage: async (roomId: string, stage: Partial) => { - Object.assign(state.rooms.get(roomId)!, stage); - }, - replaceStage: async ( - roomId: string, - expectedShareId: string | null, - stage: Partial, - ) => { - const room = state.rooms.get(roomId)!; - if (room.stageShareId !== expectedShareId) return false; - Object.assign(room, stage); - return true; - }, - clearStage: async (roomId: string) => { - Object.assign(state.rooms.get(roomId)!, { - presenterUserId: null, - stageProtocol: null, - stageHostId: null, - stageShareId: null, - }); - }, - endRoom: async (roomId: string) => { - Object.assign(state.rooms.get(roomId)!, { - endedAt: "2026-08-25T01:00:00.000Z", - presenterUserId: null, - stageProtocol: null, - stageHostId: null, - stageShareId: null, - }); - }, - }), - createCurrentSessionShareRepository: () => ({ - create: async (input: Record) => { - const row = { ...input, revokedAt: null }; - state.shares.set(input.id as string, row); - return row; - }, - findActiveById: async (id: string) => { - const share = state.shares.get(id); - return share && !share.revokedAt ? share : null; - }, - revokeAsAdmin: async (id: string) => { - const share = state.shares.get(id); - if (!share) return false; - share.revokedAt = "now"; - return true; - }, - }), - createCurrentUserRepository: () => ({ - findById: async (id: string) => (state.users.has(id) ? { id } : null), - }), - createCurrentRoleRepository: () => ({ - findRoleById: async (id: number) => (state.roles.has(id) ? { id } : null), - listRoleUserIds: async (id: number) => state.roles.get(id) ?? [], - }), -})); - -const { default: router } = await import("../../../hosts/collab/routes.js"); -const { getStageController } = - await import("../../../hosts/collab/stage-control.js"); - -type RouteLayer = { - route?: { - path: string; - methods: Record; - stack: { - handle: (req: Request, res: Response, next: () => void) => unknown; - }[]; - }; -}; - -async function invoke( - method: string, - path: string, - overrides: { - body?: Record; - params?: Record; - ip?: string; - } = {}, -) { - const layers = (router as unknown as { stack: RouteLayer[] }).stack; - const layer = layers.find( - (l) => l.route?.path === path && l.route.methods[method], - ); - if (!layer?.route) throw new Error(`No route for ${method} ${path}`); - const req = { - body: overrides.body ?? {}, - params: overrides.params ?? {}, - headers: {}, - ip: overrides.ip ?? "127.0.0.1", - socket: { remoteAddress: overrides.ip ?? "127.0.0.1" }, - } as unknown as Request; - const res = { - statusCode: 200, - jsonBody: null as unknown, - status(code: number) { - this.statusCode = code; - return this; - }, - json(payload: unknown) { - this.jsonBody = payload; - return this; - }, - }; - for (const handler of layer.route.stack) { - let calledNext = false; - await handler.handle(req, res as unknown as Response, () => { - calledNext = true; - }); - if (!calledNext) break; - } - return res as { - statusCode: number; - jsonBody: Record | null; - }; -} - -async function as(userId: string, run: () => Promise) { - const previous = state.currentUserId; - state.currentUserId = userId; - try { - return await run(); - } finally { - state.currentUserId = previous; - } -} - -async function createRoom(persistent = false): Promise { - const response = await invoke("post", "/rooms", { - body: { name: "Standup", persistent }, - }); - return (response.jsonBody!.room as Room).id; -} - -async function invite(roomId: string, userIds: string[]) { - return invoke("post", "/rooms/:id/members", { - params: { id: roomId }, - body: { userIds }, - }); -} - -async function present(roomId: string, sessionId: string, protocol = "ssh") { - return invoke("post", "/rooms/:id/present", { - params: { id: roomId }, - body: { protocol, sessionId, hostId: 1 }, - }); -} - -describe("collab room routes", () => { - beforeEach(() => { - state.currentUserId = "host-1"; - state.rooms.clear(); - state.members.clear(); - state.shares.clear(); - state.liveOwned.clear(); - state.broadcasts.length = 0; - state.control.length = 0; - state.controllers.clear(); - state.requests.clear(); - state.sharingEnabled = true; - }); - - it("creating a room makes the creator its host and lists it for them only", async () => { - const roomId = await createRoom(); - expect(state.members.get(`${roomId}:host-1`)?.roomRole).toBe("host"); - - const mine = await invoke("get", "/rooms"); - expect((mine.jsonBody!.rooms as Room[]).map((r) => r.id)).toEqual([roomId]); - - const other = await as("alice", () => - invoke("get", "/rooms/:id", { params: { id: roomId } }), - ); - expect((other as { statusCode: number }).statusCode).toBe(404); - }); - - it("never exposes the guest bearer token in room responses", async () => { - const roomId = await createRoom(); - await invite(roomId, ["alice"]); - state.rooms.get(roomId)!.guestLinkToken = "secret-token"; - - const list = await as("alice", () => invoke("get", "/rooms")); - const listedRoom = (list as Awaited>).jsonBody! - .rooms as Array>; - expect(listedRoom[0]).not.toHaveProperty("guestLinkToken"); - expect(listedRoom[0]).toHaveProperty("guestLinkEnabled", true); - - const detail = await as("alice", () => - invoke("get", "/rooms/:id", { params: { id: roomId } }), - ); - const room = (detail as Awaited>).jsonBody! - .room as Record; - expect(room).not.toHaveProperty("guestLinkToken"); - expect(room).toHaveProperty("guestLinkEnabled", true); - }); - - it("rejects an empty or oversized room name", async () => { - expect( - (await invoke("post", "/rooms", { body: { name: " " } })).statusCode, - ).toBe(400); - expect( - (await invoke("post", "/rooms", { body: { name: "x".repeat(121) } })) - .statusCode, - ).toBe(400); - }); - - it("only the host invites; roles expand to their members; unknown users 404", async () => { - const roomId = await createRoom(); - await invite(roomId, ["alice"]); - const byMember = await as("alice", () => invite(roomId, ["bob"])); - expect((byMember as { statusCode: number }).statusCode).toBe(403); - - expect((await invite(roomId, ["nobody"])).statusCode).toBe(404); - expect( - ( - await invoke("post", "/rooms/:id/members", { - params: { id: roomId }, - body: {}, - }) - ).statusCode, - ).toBe(400); - - const byRole = await invoke("post", "/rooms/:id/members", { - params: { id: roomId }, - body: { roleIds: [7] }, - }); - expect(byRole.statusCode).toBe(200); - expect(state.members.has(`${roomId}:carol`)).toBe(true); - expect(state.broadcasts.at(-1)).toMatchObject({ - type: "collab_members_changed", - }); - }); - - it("members may leave, only the host removes others, the owner is never removed", async () => { - const roomId = await createRoom(); - await invite(roomId, ["alice", "bob"]); - - const aliceRemovesBob = await as("alice", () => - invoke("delete", "/rooms/:id/members/:userId", { - params: { id: roomId, userId: "bob" }, - }), - ); - expect((aliceRemovesBob as { statusCode: number }).statusCode).toBe(403); - - await as("alice", () => - invoke("delete", "/rooms/:id/members/:userId", { - params: { id: roomId, userId: "alice" }, - }), - ); - expect(state.members.has(`${roomId}:alice`)).toBe(false); - - const removeOwner = await invoke("delete", "/rooms/:id/members/:userId", { - params: { id: roomId, userId: "host-1" }, - }); - expect(removeOwner.statusCode).toBe(400); - }); - - it("presenting validates the protocol, the sharing toggle and live-session ownership", async () => { - const roomId = await createRoom(); - expect((await present(roomId, "s1", "ftp")).statusCode).toBe(400); - expect((await present(roomId, "s1")).statusCode).toBe(403); // not live - - state.liveOwned.set("s1", "host-1"); - state.sharingEnabled = false; - expect((await present(roomId, "s1")).statusCode).toBe(403); - state.sharingEnabled = true; - - const ok = await present(roomId, "s1"); - expect(ok.statusCode).toBe(200); - const room = state.rooms.get(roomId)!; - expect(room.presenterUserId).toBe("host-1"); - expect(state.shares.get(room.stageShareId!)).toMatchObject({ - shareType: "room", - permissionLevel: "read-only", - sessionId: "s1", - }); - expect(state.broadcasts.at(-1)).toMatchObject({ - type: "collab_stage_changed", - }); - }); - - it("a takeover revokes the previous stage share and clears control", async () => { - const roomId = await createRoom(); - await invite(roomId, ["alice"]); - state.liveOwned.set("s1", "host-1"); - state.liveOwned.set("s2", "alice"); - await present(roomId, "s1"); - const firstShare = state.rooms.get(roomId)!.stageShareId!; - await invoke("post", "/rooms/:id/control", { - params: { id: roomId }, - body: { userId: "alice" }, - }); - expect(await getStageController(roomId)).toBe("alice"); - - await as("alice", () => present(roomId, "s2")); - const room = state.rooms.get(roomId)!; - expect(room.presenterUserId).toBe("alice"); - expect(state.shares.get(firstShare)!.revokedAt).toBeTruthy(); - expect(await getStageController(roomId)).toBeNull(); - }); - - it("stop is for the presenter or host", async () => { - const roomId = await createRoom(); - await invite(roomId, ["alice", "bob"]); - state.liveOwned.set("s2", "alice"); - await as("alice", () => present(roomId, "s2")); - - const bob = await as("bob", () => - invoke("post", "/rooms/:id/stop", { params: { id: roomId } }), - ); - expect((bob as { statusCode: number }).statusCode).toBe(403); - await invoke("post", "/rooms/:id/stop", { params: { id: roomId } }); // host - expect(state.rooms.get(roomId)!.stageShareId).toBeNull(); - }); - - it("keeps remote desktop stages read-only", async () => { - const roomId = await createRoom(); - await invite(roomId, ["alice"]); - const stageOf = (user: string) => - as(user, () => - invoke("get", "/rooms/:id/stage", { params: { id: roomId } }), - ) as Promise<{ - jsonBody: { stage: Record | null }; - }>; - - expect((await stageOf("alice")).jsonBody.stage).toBeNull(); - - state.liveOwned.set("s1", "host-1"); - await present(roomId, "s1"); - const ssh = (await stageOf("alice")).jsonBody.stage!; - expect(ssh).toMatchObject({ protocol: "ssh", sessionId: "s1" }); - expect(ssh.connectParams).toBeUndefined(); - - state.liveOwned.set("g1", "host-1"); - await present(roomId, "g1", "rdp"); - expect((await stageOf("alice")).jsonBody.stage!.connectParams).toEqual({ - token: "join:g1:true", - }); - const control = await invoke("post", "/rooms/:id/control", { - params: { id: roomId }, - body: { userId: "alice" }, - }); - expect(control.statusCode).toBe(400); - expect((await stageOf("alice")).jsonBody.stage!.connectParams).toEqual({ - token: "join:g1:true", - }); - }); - - it("a dead presenter session clears the stale stage on resolve", async () => { - const roomId = await createRoom(); - await invite(roomId, ["alice"]); - state.liveOwned.set("s1", "host-1"); - await present(roomId, "s1"); - state.liveOwned.delete("s1"); - - const resolved = await as("alice", () => - invoke("get", "/rooms/:id/stage", { params: { id: roomId } }), - ); - expect( - (resolved as { jsonBody: { stage: unknown } }).jsonBody.stage, - ).toBeNull(); - expect(state.rooms.get(roomId)!.stageShareId).toBeNull(); - expect(state.broadcasts.at(-1)).toMatchObject({ - type: "collab_stage_changed", - stage: null, - }); - }); - - it("control: presenter/host grant, controller releases, members may only ask", async () => { - const roomId = await createRoom(); - await invite(roomId, ["alice", "bob"]); - const control = (user: string, target: string | null) => - as(user, () => - invoke("post", "/rooms/:id/control", { - params: { id: roomId }, - body: { userId: target }, - }), - ) as Promise<{ statusCode: number }>; - - expect((await control("host-1", "alice")).statusCode).toBe(400); // nothing presented - - state.liveOwned.set("s1", "host-1"); - await present(roomId, "s1"); - expect((await control("bob", "bob")).statusCode).toBe(403); - expect((await control("host-1", "nobody")).statusCode).toBe(404); - expect((await control("host-1", "alice")).statusCode).toBe(200); - expect(state.control.at(-1)).toEqual([ - "s1", - state.rooms.get(roomId)!.stageShareId, - "alice", - ]); - expect(state.broadcasts).toContainEqual( - expect.objectContaining({ - type: "collab_control_changed", - controllerUserId: "alice", - }), - ); - - expect((await control("bob", null)).statusCode).toBe(403); - expect((await control("alice", null)).statusCode).toBe(200); - expect(await getStageController(roomId)).toBeNull(); - - const asked = await as("bob", () => - invoke("post", "/rooms/:id/control/request", { params: { id: roomId } }), - ); - expect((asked as { statusCode: number }).statusCode).toBe(200); - expect(state.broadcasts.at(-1)).toMatchObject({ - type: "collab_control_requested", - userId: "bob", - username: "BOB", - }); - expect( - ( - await as("bob", () => - invoke("get", "/rooms/:id", { params: { id: roomId } }), - ) - ).jsonBody!.controlRequests, - ).toEqual([expect.objectContaining({ userId: "bob" })]); - - const repeated = await as("bob", () => - invoke("post", "/rooms/:id/control/request", { - params: { id: roomId }, - }), - ); - expect((repeated as { statusCode: number }).statusCode).toBe(429); - - const listed = await invoke("get", "/rooms/:id/control/requests", { - params: { id: roomId }, - }); - expect(listed.jsonBody!.requests).toEqual([ - expect.objectContaining({ userId: "bob" }), - ]); - await invoke("delete", "/rooms/:id/control/requests/:userId", { - params: { id: roomId, userId: "bob" }, - }); - expect(state.requests.get(roomId)?.size ?? 0).toBe(0); - }); - - it("guest link: host-only toggle, anonymous resolve follows the stage, rate limited", async () => { - const roomId = await createRoom(); - await invite(roomId, ["alice"]); - const toggle = (user: string, enabled: boolean) => - as(user, () => - invoke("post", "/rooms/:id/guest-link", { - params: { id: roomId }, - body: { enabled }, - }), - ) as Promise<{ - statusCode: number; - jsonBody: { guestLinkToken: string | null }; - }>; - - expect((await toggle("alice", true)).statusCode).toBe(403); - const { guestLinkToken } = (await toggle("host-1", true)).jsonBody; - expect(guestLinkToken).toBeTruthy(); - - const resolve = (token: string, ip = "9.9.9.9") => - invoke("get", "/guest/:token", { params: { token }, ip }); - expect((await resolve("nope")).statusCode).toBe(404); - expect((await resolve(guestLinkToken!)).jsonBody).toEqual({ - roomName: "Standup", - stage: null, - }); - - state.liveOwned.set("s1", "host-1"); - await present(roomId, "s1"); - expect((await resolve(guestLinkToken!)).jsonBody!.stage).toMatchObject({ - protocol: "ssh", - wsPath: `/plugin-ws/ssh-terminal/terminal?roomGuestToken=${encodeURIComponent(guestLinkToken!)}`, - }); - state.liveOwned.set("g1", "host-1"); - await present(roomId, "g1", "vnc"); - expect((await resolve(guestLinkToken!)).jsonBody!.stage).toMatchObject({ - protocol: "vnc", - connectParams: { token: "join:g1:true" }, - }); - - await toggle("host-1", false); - expect((await resolve(guestLinkToken!)).statusCode).toBe(404); - - let last = 200; - for (let i = 0; i < 61; i++) { - last = (await resolve("x", "1.2.3.4")).statusCode; - } - expect(last).toBe(429); - }); - - it("ending a one-off room ends it; ending a persistent room only clears the stage", async () => { - const oneOff = await createRoom(false); - await invite(oneOff, ["alice"]); - const byMember = await as("alice", () => - invoke("post", "/rooms/:id/end", { params: { id: oneOff } }), - ); - expect((byMember as { statusCode: number }).statusCode).toBe(403); - await invoke("post", "/rooms/:id/end", { params: { id: oneOff } }); - expect(state.rooms.get(oneOff)!.endedAt).toBeTruthy(); - expect(state.broadcasts.at(-1)).toMatchObject({ - type: "collab_room_ended", - }); - - const persistent = await createRoom(true); - state.liveOwned.set("s1", "host-1"); - await present(persistent, "s1"); - await invoke("post", "/rooms/:id/end", { params: { id: persistent } }); - const room = state.rooms.get(persistent)!; - expect(room.endedAt).toBeNull(); - expect(room.stageShareId).toBeNull(); - }); -}); diff --git a/src/backend/tests/hosts/collab/runtime-store.test.ts b/src/backend/tests/hosts/collab/runtime-store.test.ts deleted file mode 100644 index 2443eacbf..000000000 --- a/src/backend/tests/hosts/collab/runtime-store.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { collabRuntimeStore } from "../../../hosts/collab/runtime-store.js"; - -describe("collabRuntimeStore local fallback", () => { - it("stores stage control and an ordered, deduplicated request queue", async () => { - const roomId = `fallback-${crypto.randomUUID()}`; - - await collabRuntimeStore.setController(roomId, "alice"); - expect(await collabRuntimeStore.getController(roomId)).toBe("alice"); - - await collabRuntimeStore.upsertRequest(roomId, { - userId: "bob", - username: "Bob", - requestedAt: "2026-08-25T00:00:02.000Z", - }); - await collabRuntimeStore.upsertRequest(roomId, { - userId: "alice", - username: "Alice", - requestedAt: "2026-08-25T00:00:01.000Z", - }); - await collabRuntimeStore.upsertRequest(roomId, { - userId: "bob", - username: "Bob", - requestedAt: "2026-08-25T00:00:03.000Z", - }); - - expect(await collabRuntimeStore.listRequests(roomId)).toEqual([ - expect.objectContaining({ userId: "alice" }), - expect.objectContaining({ userId: "bob" }), - ]); - await collabRuntimeStore.removeRequest(roomId, "alice"); - expect(await collabRuntimeStore.listRequests(roomId)).toHaveLength(1); - - await collabRuntimeStore.clearRequests(roomId); - await collabRuntimeStore.setController(roomId, null); - expect(await collabRuntimeStore.listRequests(roomId)).toEqual([]); - expect(await collabRuntimeStore.getController(roomId)).toBeNull(); - }); -}); diff --git a/src/backend/tests/hosts/session-sharing/routes.test.ts b/src/backend/tests/hosts/session-sharing/routes.test.ts deleted file mode 100644 index 086769666..000000000 --- a/src/backend/tests/hosts/session-sharing/routes.test.ts +++ /dev/null @@ -1,508 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { Request, Response } from "express"; - -const state = vi.hoisted(() => ({ - currentUserId: "user-1", - globalSharingEnabled: true, - hosts: new Map(), - hostOwnerAccess: new Map(), // `${userId}:${hostId}` -> hasAccess - sshSessions: new Map(), - guacSessions: new Map< - string, - { ownerUserId: string; hostId: number; protocol: string } - >(), - shares: new Map>(), - admins: new Set(), -})); - -vi.mock("../../../utils/logger.js", () => ({ - sshLogger: { - error: vi.fn(), - warn: vi.fn(), - info: vi.fn(), - success: vi.fn(), - }, -})); - -vi.mock("../../../utils/auth-manager.js", () => ({ - AuthManager: { - getInstance: () => ({ - createAuthMiddleware: - () => - (req: Record, _res: unknown, next: () => void) => { - req.userId = state.currentUserId; - next(); - }, - }), - }, -})); - -vi.mock("../../../utils/permission-manager.js", () => ({ - PermissionManager: { - getInstance: () => ({ - canAccessHost: async ( - userId: string, - hostId: number, - _action: string, - ) => ({ - hasAccess: state.hostOwnerAccess.get(`${userId}:${hostId}`) ?? false, - }), - isAdmin: async (userId: string) => state.admins.has(userId), - }), - }, -})); - -vi.mock("../../../hosts/live-terminal-sessions.js", () => ({ - liveTerminalSessions: { - getSession: (sessionId: string) => { - const session = state.sshSessions.get(sessionId); - if (!session) return null; - return { ...session }; - }, - ownerEndSession: vi.fn(), - }, -})); - -vi.mock("../../../hosts/guacamole-sessions.js", () => ({ - getGuacamoleSessionInfo: (guacamoleConnectionId: string) => - state.guacSessions.get(guacamoleConnectionId) ?? null, - createGuacamoleJoinToken: ( - guacamoleConnectionId: string, - readOnly: boolean, - ) => `join-token:${guacamoleConnectionId}:${readOnly}`, -})); - -vi.mock("../../../database/repositories/factory.js", () => ({ - createCurrentSessionShareRepository: () => ({ - create: async (input: Record) => { - const row = { - ...input, - createdAt: "2026-07-20T00:00:00.000Z", - revokedAt: null, - lastJoinedAt: null, - joinCount: 0, - }; - state.shares.set(input.id as string, row); - return row; - }, - findById: async (id: string) => state.shares.get(id) ?? null, - findByLinkToken: async (linkToken: string) => { - for (const share of state.shares.values()) { - if ( - share.linkToken === linkToken && - !share.revokedAt && - (share.expiresAt as string) > new Date().toISOString() - ) { - return share; - } - } - return null; - }, - findActiveSharesForHost: async (hostId: number, ownerUserId: string) => { - return [...state.shares.values()].filter( - (s) => - s.hostId === hostId && s.ownerUserId === ownerUserId && !s.revokedAt, - ); - }, - revoke: async (shareId: string, requestingUserId: string) => { - const share = state.shares.get(shareId); - if (!share || share.ownerUserId !== requestingUserId) return false; - share.revokedAt = "2026-07-20T01:00:00.000Z"; - return true; - }, - revokeAsAdmin: async (shareId: string) => { - const share = state.shares.get(shareId); - if (!share) return false; - share.revokedAt = "2026-07-20T01:00:00.000Z"; - return true; - }, - touchShareUsage: async () => {}, - recordParticipantJoin: async () => ({ id: 1 }), - }), - createCurrentSettingsRepository: () => ({ - getBoolean: async () => state.globalSharingEnabled, - }), - createCurrentHostResolutionRepository: () => ({ - findHostOwnerId: async (hostId: number) => - state.hosts.get(hostId)?.userId ?? null, - findHostById: async (hostId: number) => { - const host = state.hosts.get(hostId); - if (!host) return null; - return { allowSessionSharing: host.allowSessionSharing }; - }, - }), -})); - -const { default: router } = - await import("../../../hosts/session-sharing/routes.js"); - -type RouteLayer = { - route?: { - path: string; - methods: Record; - stack: { - handle: (req: Request, res: Response, next: () => void) => unknown; - }[]; - }; -}; - -function findHandlers(method: string, path: string) { - const layers = (router as unknown as { stack: RouteLayer[] }).stack; - const layer = layers.find( - (l) => l.route?.path === path && l.route.methods[method], - ); - if (!layer?.route) throw new Error(`No route for ${method} ${path}`); - return layer.route.stack.map((s) => s.handle); -} - -function makeReqRes(overrides: { - body?: Record; - params?: Record; - ip?: string; -}) { - const req = { - body: overrides.body ?? {}, - params: overrides.params ?? {}, - headers: {}, - ip: overrides.ip ?? "127.0.0.1", - socket: { remoteAddress: overrides.ip ?? "127.0.0.1" }, - } as unknown as Request; - - const res = { - statusCode: 200, - jsonBody: null as unknown, - status(code: number) { - (this as unknown as { statusCode: number }).statusCode = code; - return this; - }, - json(payload: unknown) { - (this as unknown as { jsonBody: unknown }).jsonBody = payload; - return this; - }, - } as unknown as Response & { statusCode: number; jsonBody: unknown }; - - return { req, res }; -} - -async function invoke( - method: string, - path: string, - overrides: { - body?: Record; - params?: Record; - ip?: string; - } = {}, -) { - const handlers = findHandlers(method, path); - const { req, res } = makeReqRes(overrides); - - for (const handler of handlers) { - let calledNext = false; - await handler(req, res, () => { - calledNext = true; - }); - if (!calledNext) break; - } - - return res as unknown as { - statusCode: number; - jsonBody: Record | null; - }; -} - -beforeEach(() => { - state.currentUserId = "user-1"; - state.globalSharingEnabled = true; - state.hosts = new Map([ - [1, { userId: "user-1", allowSessionSharing: true }], - [2, { userId: "user-1", allowSessionSharing: false }], - ]); - state.hostOwnerAccess = new Map([["user-2:1", true]]); - state.sshSessions = new Map([ - ["session-1", { userId: "user-1", isConnected: true }], - ]); - state.guacSessions = new Map([ - ["guac-conn-1", { ownerUserId: "user-1", hostId: 1, protocol: "vnc" }], - ]); - state.shares = new Map(); - state.admins = new Set(); -}); - -describe("POST /session-sharing/create", () => { - it("rejects a caller who does not own the live session", async () => { - state.currentUserId = "user-2"; - const res = await invoke("post", "/create", { - body: { - hostId: 1, - sessionId: "session-1", - protocol: "ssh", - shareType: "link", - permissionLevel: "read-only", - }, - }); - - expect(res.statusCode).toBe(403); - expect(res.jsonBody).toMatchObject({ - error: "You do not own this live session", - }); - }); - - it("creates a link share for the session owner", async () => { - const res = await invoke("post", "/create", { - body: { - hostId: 1, - sessionId: "session-1", - protocol: "ssh", - shareType: "link", - permissionLevel: "read-only", - }, - }); - - expect(res.statusCode).toBe(200); - expect(res.jsonBody).toMatchObject({ shareId: expect.any(String) }); - expect((res.jsonBody as Record).linkToken).toBeTruthy(); - }); - - it("rejects a user share when the target lacks host access", async () => { - const res = await invoke("post", "/create", { - body: { - hostId: 1, - sessionId: "session-1", - protocol: "ssh", - shareType: "user", - targetUserId: "no-access-user", - permissionLevel: "read-write", - }, - }); - - expect(res.statusCode).toBe(403); - expect(res.jsonBody).toMatchObject({ - error: "Target user does not have access to this host", - }); - }); - - it("global kill switch overrides an enabled per-host toggle", async () => { - state.globalSharingEnabled = false; - const res = await invoke("post", "/create", { - body: { - hostId: 1, - sessionId: "session-1", - protocol: "ssh", - shareType: "link", - permissionLevel: "read-only", - }, - }); - - expect(res.statusCode).toBe(403); - expect(res.jsonBody).toMatchObject({ - error: "Session sharing is disabled for this host", - }); - }); - - it("rejects when the per-host toggle is off even though global is on", async () => { - const res = await invoke("post", "/create", { - body: { - hostId: 2, - sessionId: "session-1", - protocol: "ssh", - shareType: "link", - permissionLevel: "read-only", - }, - }); - - expect(res.statusCode).toBe(403); - }); -}); - -describe("GET /session-sharing/resolve/:linkToken", () => { - async function createActiveLinkShare( - overrides: Partial> = {}, - ) { - await invoke("post", "/create", { - body: { - hostId: 1, - sessionId: "session-1", - protocol: "ssh", - shareType: "link", - permissionLevel: "read-only", - ...overrides, - }, - }); - const [share] = [...state.shares.values()]; - return share as { linkToken: string; id: string }; - } - - it("never includes hostname, ip, username, or hostId in the response body", async () => { - const share = await createActiveLinkShare(); - - const res = await invoke("get", "/resolve/:linkToken", { - params: { linkToken: share.linkToken }, - }); - - expect(res.statusCode).toBe(200); - const body = res.jsonBody as Record; - const serialized = JSON.stringify(body).toLowerCase(); - - expect(body).not.toHaveProperty("hostname"); - expect(body).not.toHaveProperty("ip"); - expect(body).not.toHaveProperty("username"); - expect(body).not.toHaveProperty("hostId"); - expect(body).not.toHaveProperty("hostName"); - expect(serialized).not.toContain("10.0.0"); - expect(serialized).not.toContain("hostname"); - expect(serialized).not.toContain('"ip"'); - expect(serialized).not.toContain("username"); - }); - - it("returns only protocol/permissionLevel/wsPath(/connectParams) for ssh", async () => { - const share = await createActiveLinkShare(); - - const res = await invoke("get", "/resolve/:linkToken", { - params: { linkToken: share.linkToken }, - }); - - expect(res.jsonBody).toEqual({ - protocol: "ssh", - permissionLevel: "read-only", - wsPath: `/plugin-ws/ssh-terminal/terminal?shareToken=${encodeURIComponent(share.linkToken)}`, - }); - }); - - it("mints a fresh join token for guac protocols", async () => { - await invoke("post", "/create", { - body: { - hostId: 1, - sessionId: "guac-conn-1", - protocol: "vnc", - shareType: "link", - permissionLevel: "read-only", - }, - }); - const [share] = [...state.shares.values()] as { - linkToken: string; - }[]; - - const res = await invoke("get", "/resolve/:linkToken", { - params: { linkToken: share.linkToken }, - }); - - expect(res.statusCode).toBe(200); - expect((res.jsonBody as Record).connectParams).toEqual({ - token: "join-token:guac-conn-1:true", - }); - }); - - it("rejects an unknown link token", async () => { - const res = await invoke("get", "/resolve/:linkToken", { - params: { linkToken: "does-not-exist" }, - }); - - expect(res.statusCode).toBe(404); - }); - - it("rejects a revoked link token", async () => { - const share = await createActiveLinkShare(); - await invoke("delete", "/:shareId", { params: { shareId: share.id } }); - - const res = await invoke("get", "/resolve/:linkToken", { - params: { linkToken: share.linkToken }, - }); - - expect(res.statusCode).toBe(404); - }); - - it("rejects an expired link token", async () => { - state.shares.set("share-expired", { - id: "share-expired", - hostId: 1, - ownerUserId: "user-1", - protocol: "ssh", - sessionId: "session-1", - shareType: "link", - linkToken: "expired-token", - permissionLevel: "read-only", - expiresAt: "2000-01-01T00:00:00.000Z", - revokedAt: null, - }); - - const res = await invoke("get", "/resolve/:linkToken", { - params: { linkToken: "expired-token" }, - }); - - expect(res.statusCode).toBe(404); - }); - - it("re-checks the global kill switch at resolve time, not just at creation time", async () => { - const share = await createActiveLinkShare(); - - state.globalSharingEnabled = false; - - const res = await invoke("get", "/resolve/:linkToken", { - params: { linkToken: share.linkToken }, - }); - - expect(res.statusCode).toBe(404); - }); -}); - -describe("DELETE /session-sharing/:shareId", () => { - it("allows the owner to revoke their own share", async () => { - await invoke("post", "/create", { - body: { - hostId: 1, - sessionId: "session-1", - protocol: "ssh", - shareType: "link", - permissionLevel: "read-only", - }, - }); - const [share] = [...state.shares.values()] as { id: string }[]; - - const res = await invoke("delete", "/:shareId", { - params: { shareId: share.id }, - }); - - expect(res.statusCode).toBe(200); - }); - - it("rejects a non-owner, non-admin caller", async () => { - await invoke("post", "/create", { - body: { - hostId: 1, - sessionId: "session-1", - protocol: "ssh", - shareType: "link", - permissionLevel: "read-only", - }, - }); - const [share] = [...state.shares.values()] as { id: string }[]; - - state.currentUserId = "user-2"; - const res = await invoke("delete", "/:shareId", { - params: { shareId: share.id }, - }); - - expect(res.statusCode).toBe(403); - }); - - it("allows an admin to revoke someone else's share", async () => { - await invoke("post", "/create", { - body: { - hostId: 1, - sessionId: "session-1", - protocol: "ssh", - shareType: "link", - permissionLevel: "read-only", - }, - }); - const [share] = [...state.shares.values()] as { id: string }[]; - - state.currentUserId = "admin-1"; - state.admins.add("admin-1"); - const res = await invoke("delete", "/:shareId", { - params: { shareId: share.id }, - }); - - expect(res.statusCode).toBe(200); - }); -}); diff --git a/src/backend/tests/plugins/ctx-services.test.ts b/src/backend/tests/plugins/ctx-services.test.ts new file mode 100644 index 000000000..e21e20d1b --- /dev/null +++ b/src/backend/tests/plugins/ctx-services.test.ts @@ -0,0 +1,84 @@ +/** + * ctx.services with named providers: a plugin may only register the names + * its manifest lists, a consumer reaches one provider by name, and each is + * disposed with the plugin that provided it. + */ + +import { afterEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../../utils/audit-logger.js", () => ({ logAudit: async () => {} })); + +import { createPluginContext, createPluginHandle } from "../../plugins/ctx.js"; +import { + clearServiceRegistry, + getRegistration, +} from "../../plugins/service-registry.js"; +import type { PluginManifest } from "@termix/plugin-sdk/manifest"; + +function contextFor(pluginId: string, names?: string[]) { + const manifest = { + id: pluginId, + name: pluginId, + version: "1.0.0", + description: "", + author: { name: "test" }, + license: "MIT", + category: "Productivity", + engine: { termix: ">=2.9.0", api: "1" }, + capabilities: [], + provides: [ + { + service: "sample.live", + version: "1.0.0", + permission: `${pluginId}.use`, + ...(names ? { names } : {}), + }, + ], + } as PluginManifest; + const handle = createPluginHandle(pluginId, { activate: () => {} }); + return { ctx: createPluginContext(manifest, handle), handle }; +} + +afterEach(() => clearServiceRegistry()); + +describe("ctx.services named providers", () => { + it("registers a declared name and lists it", () => { + const { ctx } = contextFor("terminal-like", ["ssh"]); + ctx.services.provide("sample.live", { get: () => 1 }, { name: "ssh" }); + + expect(getRegistration("sample.live", "ssh")?.pluginId).toBe( + "terminal-like", + ); + expect(ctx.services.providers("sample.live")).toEqual(["ssh"]); + }); + + it("refuses a name the manifest does not list", () => { + const { ctx } = contextFor("terminal-like", ["ssh"]); + expect(() => + ctx.services.provide("sample.live", {}, { name: "rdp" }), + ).toThrow(/provides\[\]\.names does not list it/); + expect(() => ctx.services.provide("sample.live", {})).toThrow( + /does not list it/, + ); + }); + + it("refuses a name when the manifest declares an unnamed service", () => { + const { ctx } = contextFor("plain"); + expect(() => + ctx.services.provide("sample.live", {}, { name: "ssh" }), + ).toThrow(/does not list it/); + }); + + it("reaches one provider by name through a handle", () => { + const { ctx } = contextFor("terminal-like", ["ssh"]); + ctx.services.provide("sample.live", { get: () => 1 }, { name: "ssh" }); + + const named = ctx.services.get<{ get: () => Promise }>( + "sample.live", + { provider: "ssh" }, + ); + const other = ctx.services.get("sample.live", { provider: "rdp" }); + expect("get" in named).toBe(true); + expect("get" in other).toBe(false); + }); +}); diff --git a/src/backend/tests/plugins/manifest.test.ts b/src/backend/tests/plugins/manifest.test.ts index 3fab8fda6..901c4b1ca 100644 --- a/src/backend/tests/plugins/manifest.test.ts +++ b/src/backend/tests/plugins/manifest.test.ts @@ -589,6 +589,19 @@ describe("panels, dashboard cards and guest pages", () => { expect(errors).toMatch(/dashboardCards\[0\]\.titleKey/); }); + it("accepts guest views on a guest plugin and refuses them otherwise", () => { + expect( + validateManifest( + base({ contributes: { guest: true, guestViews: ["shared"] } }), + ), + ).toEqual([]); + const errors = validateManifest( + base({ contributes: { guestViews: ["Shared View"] } }), + ).join(); + expect(errors).toMatch(/contributes\.guestViews entries must be/); + expect(errors).toMatch(/needs "contributes\.guest"/); + }); + it("rejects an unknown field inside a view and a non-boolean guest", () => { const errors = validateManifest( base({ @@ -602,3 +615,41 @@ describe("panels, dashboard cards and guest pages", () => { expect(errors).toMatch(/contributes\.guest" must be a boolean/); }); }); + +describe("named service providers", () => { + const provides = (names: unknown) => [ + { + service: "sample.live", + version: "1.0.0", + permission: "sample-plugin.use", + names, + }, + ]; + const permissions = { + permissions: [ + { + name: "use", + titleKey: "permissions.use.title", + descriptionKey: "permissions.use.description", + }, + ], + }; + + it("accepts a list of provider names", () => { + expect( + validateManifest( + base({ provides: provides(["ssh", "rdp"]), contributes: permissions }), + ), + ).toEqual([]); + }); + + it("refuses an empty, duplicated or malformed list", () => { + for (const names of [[], ["ssh", "ssh"], ["SSH"], "ssh"]) { + expect( + validateManifest( + base({ provides: provides(names), contributes: permissions }), + ).join(), + ).toMatch(/provides\[0\]\.names/); + } + }); +}); diff --git a/src/backend/tests/plugins/service-registry.test.ts b/src/backend/tests/plugins/service-registry.test.ts index 1d6c14f5e..61590c37a 100644 --- a/src/backend/tests/plugins/service-registry.test.ts +++ b/src/backend/tests/plugins/service-registry.test.ts @@ -22,6 +22,7 @@ const { createServiceHandle, getRegistration, getServiceImplementation, + listProviderNames, listServices, provideService, resolveRequirements, @@ -99,6 +100,94 @@ describe("service registry", () => { ).toBeUndefined(); }); + describe("named providers", () => { + it("keeps several providers of one service side by side", () => { + const ssh = register({ + name: "ssh", + implementation: { kind: async () => "ssh" }, + }); + const rdp = register({ + name: "rdp", + pluginId: "other-plugin", + implementation: { kind: async () => "rdp" }, + }); + + expect(listProviderNames("testplugin.greet").sort()).toEqual([ + "rdp", + "ssh", + ]); + expect(getRegistration("testplugin.greet", "ssh")).toBe(ssh); + expect(getRegistration("testplugin.greet", "rdp")).toBe(rdp); + expect(getRegistration("testplugin.greet")).toBeUndefined(); + expect(listServices()).toHaveLength(2); + }); + + it("hands core a named implementation only by its name", () => { + const ssh = register({ name: "ssh" }); + expect( + getServiceImplementation("testplugin.greet", "^1.0.0", "ssh"), + ).toBe(ssh.implementation); + expect( + getServiceImplementation("testplugin.greet", "^1.0.0"), + ).toBeUndefined(); + }); + + it("revokes one provider and leaves the other", () => { + const ssh = register({ name: "ssh" }); + const rdp = register({ name: "rdp" }); + + expect(revokeService("testplugin.greet", ssh)).toBe(true); + expect(getRegistration("testplugin.greet", "ssh")).toBeUndefined(); + expect(getRegistration("testplugin.greet", "rdp")).toBe(rdp); + }); + + it("routes a named handle to its provider and fails typed once it goes", async () => { + const ssh = register({ + name: "ssh", + implementation: { hello: async () => "from ssh" }, + }); + register({ + name: "rdp", + implementation: { hello: async () => "from rdp" }, + }); + const context = { + resolveUserId: () => "user-1", + hasPermission: async () => true, + audit: () => {}, + }; + const handle = createServiceHandle<{ hello: () => Promise }>( + "testplugin.greet", + "consumer-plugin", + context, + "ssh", + ); + const unnamed = createServiceHandle<{ hello?: () => Promise }>( + "testplugin.greet", + "consumer-plugin", + context, + ); + + await expect(handle.hello()).resolves.toBe("from ssh"); + expect("hello" in unnamed).toBe(false); + + revokeService("testplugin.greet", ssh); + expect(() => handle.hello()).toThrow(PluginServiceUnavailableError); + }); + + it("satisfies a requirement with any compatible provider", () => { + register({ name: "rdp", version: "2.0.0" }); + register({ name: "ssh", version: "1.2.0" }); + + expect( + resolveRequirements( + manifestRequiring([ + { service: "testplugin.greet", versionRange: "^1.0.0" }, + ]), + ).satisfied, + ).toBe(true); + }); + }); + describe("resolution", () => { it("is satisfied when the version is in range", () => { register({ version: "1.2.0" }); diff --git a/src/backend/tests/utils/session-sharing-settings-migration.test.ts b/src/backend/tests/utils/session-sharing-settings-migration.test.ts new file mode 100644 index 000000000..33eebeaa5 --- /dev/null +++ b/src/backend/tests/utils/session-sharing-settings-migration.test.ts @@ -0,0 +1,151 @@ +/** + * The session sharing settings migration. + * + * An upgrade must be lossless: an instance with sharing switched off, and a + * host with sharing switched off, must stay off once the plugin reads its own + * settings. Running it twice must not duplicate or clobber what it moved. + */ + +import { beforeEach, describe, expect, it, vi } from "vitest"; + +interface SettingsRow { + pluginId: string; + scope: string; + scopeId: string | null; + key: string; + value: string | null; +} + +const blockedHostIds: number[] = []; +const coreSettings = new Map(); +const settingsRows: SettingsRow[] = []; + +const find = ( + pluginId: string, + scope: string, + scopeId: string | null, + key: string, +) => + settingsRows.find( + (row) => + row.pluginId === pluginId && + row.scope === scope && + row.scopeId === scopeId && + row.key === key, + ); + +const pluginSettingsRepository = { + get: async ( + pluginId: string, + scope: string, + scopeId: string | null, + key: string, + ) => find(pluginId, scope, scopeId, key) ?? null, + set: async ( + pluginId: string, + scope: string, + scopeId: string | null, + key: string, + value: string | null, + ) => { + const existing = find(pluginId, scope, scopeId, key); + if (existing) { + existing.value = value; + return; + } + settingsRows.push({ pluginId, scope, scopeId, key, value }); + }, +}; + +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentPluginSettingsRepository: () => pluginSettingsRepository, + createCurrentSettingsRepository: () => ({ + get: async (key: string) => coreSettings.get(key) ?? null, + }), +})); + +vi.mock("../../database/db/index.js", () => ({ + getDb: () => ({ + all: async () => blockedHostIds.map((id) => ({ id })), + }), +})); + +const { runSessionSharingSettingsMigration } = + await import("../../utils/crypto-migration/session-sharing-settings-migration.js"); + +function stored(scopeId: string | null, key: string): unknown { + const row = settingsRows.find( + (entry) => entry.scopeId === scopeId && entry.key === key, + ); + return row?.value == null ? undefined : JSON.parse(row.value); +} + +beforeEach(() => { + blockedHostIds.length = 0; + coreSettings.clear(); + settingsRows.length = 0; +}); + +describe("runSessionSharingSettingsMigration", () => { + it("moves a disabled global switch into the admin setting", async () => { + coreSettings.set("session_sharing_globally_enabled", "false"); + + const result = await runSessionSharingSettingsMigration(); + + expect(result.movedGlobal).toBe(true); + expect(stored(null, "globallyEnabled")).toBe(false); + expect(settingsRows[0]).toMatchObject({ + pluginId: "session-sharing", + scope: "admin", + }); + }); + + it("moves hosts that turned sharing off into host settings", async () => { + blockedHostIds.push(4, 9); + + const result = await runSessionSharingSettingsMigration(); + + expect(result.movedHosts).toBe(2); + expect(stored("4", "allowSessionSharing")).toBe(false); + expect(stored("9", "allowSessionSharing")).toBe(false); + }); + + it("writes nothing when sharing was on everywhere", async () => { + coreSettings.set("session_sharing_globally_enabled", "true"); + + const result = await runSessionSharingSettingsMigration(); + + expect(result).toEqual({ movedGlobal: false, movedHosts: 0, skipped: 0 }); + expect(settingsRows).toEqual([]); + }); + + it("is idempotent: a second run changes nothing", async () => { + coreSettings.set("session_sharing_globally_enabled", "false"); + blockedHostIds.push(2); + + await runSessionSharingSettingsMigration(); + const afterFirst = JSON.parse(JSON.stringify(settingsRows)); + const second = await runSessionSharingSettingsMigration(); + + expect(second.movedGlobal).toBe(false); + expect(second.movedHosts).toBe(0); + expect(second.skipped).toBe(2); + expect(settingsRows).toEqual(afterFirst); + }); + + it("does not overwrite a value the plugin already saved", async () => { + blockedHostIds.push(5); + await pluginSettingsRepository.set( + "session-sharing", + "host", + "5", + "allowSessionSharing", + "true", + ); + + const result = await runSessionSharingSettingsMigration(); + + expect(result.skipped).toBe(1); + expect(stored("5", "allowSessionSharing")).toBe(true); + }); +}); diff --git a/src/backend/utils/crypto-migration/session-sharing-settings-migration.ts b/src/backend/utils/crypto-migration/session-sharing-settings-migration.ts new file mode 100644 index 000000000..dc2769c88 --- /dev/null +++ b/src/backend/utils/crypto-migration/session-sharing-settings-migration.ts @@ -0,0 +1,107 @@ +/** + * Moves session sharing's two settings into the session-sharing plugin: + * the instance-wide switch (settings.session_sharing_globally_enabled) into + * its admin setting, and ssh_data.allow_session_sharing into its host setting. + * + * Idempotent: nothing is written where the plugin already has a row, so it is + * safe on every boot. Both default to on, so only a switch that was turned off + * needs a row. The legacy settings row is left in place for a release. + */ + +import { sql } from "drizzle-orm"; +import { databaseLogger } from "../logger.js"; +import { getDb } from "../../database/db/index.js"; +import { + createCurrentPluginSettingsRepository, + createCurrentSettingsRepository, +} from "../../database/repositories/factory.js"; + +const PLUGIN_ID = "session-sharing"; +const LEGACY_GLOBAL_KEY = "session_sharing_globally_enabled"; + +export interface SessionSharingSettingsMigrationResult { + movedGlobal: boolean; + movedHosts: number; + skipped: number; +} + +interface LegacyHostRow { + id: number; +} + +export async function runSessionSharingSettingsMigration(): Promise { + const result: SessionSharingSettingsMigrationResult = { + movedGlobal: false, + movedHosts: 0, + skipped: 0, + }; + + try { + const pluginSettings = createCurrentPluginSettingsRepository(); + + const legacy = + await createCurrentSettingsRepository().get(LEGACY_GLOBAL_KEY); + if (legacy === "false") { + const existing = await pluginSettings.get( + PLUGIN_ID, + "admin", + null, + "globallyEnabled", + ); + if (existing && existing.value !== null) { + result.skipped++; + } else { + await pluginSettings.set( + PLUGIN_ID, + "admin", + null, + "globallyEnabled", + JSON.stringify(false), + ); + result.movedGlobal = true; + } + } + + // Raw SQL, so this keeps working once schema.ts stops declaring the column. + const rows = await getDb().all(sql` + SELECT id FROM ssh_data WHERE allow_session_sharing = 0 + `); + for (const row of rows) { + const scopeId = String(row.id); + const existing = await pluginSettings.get( + PLUGIN_ID, + "host", + scopeId, + "allowSessionSharing", + ); + if (existing && existing.value !== null) { + result.skipped++; + continue; + } + await pluginSettings.set( + PLUGIN_ID, + "host", + scopeId, + "allowSessionSharing", + JSON.stringify(false), + ); + result.movedHosts++; + } + + if (result.movedGlobal || result.movedHosts > 0) { + databaseLogger.info("Moved session sharing settings into the plugin", { + operation: "session_sharing_settings_migration", + movedGlobal: result.movedGlobal, + movedHosts: result.movedHosts, + }); + } + } catch (error) { + // Never stops the backend. Sharing then stays on, the plugin's default. + databaseLogger.warn("Session sharing settings migration failed", { + operation: "session_sharing_settings_migration", + error: error instanceof Error ? error.message : String(error), + }); + } + + return result; +} diff --git a/src/backend/utils/swagger.ts b/src/backend/utils/swagger.ts index cbae8e945..a69fd21e5 100644 --- a/src/backend/utils/swagger.ts +++ b/src/backend/utils/swagger.ts @@ -145,7 +145,11 @@ const swaggerOptions: SwaggerJSDocOptions = { }, { name: "Session Sharing", - description: "Live terminal session collaboration", + description: "Share live sessions by link or with another user", + }, + { + name: "Collab", + description: "Collaboration rooms that present a live session", }, { name: "Session Logs", diff --git a/src/main.tsx b/src/main.tsx index dadd9f05f..c3e8e79eb 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -24,7 +24,7 @@ import { useTranslation } from "react-i18next"; import { UiPreferencesProvider } from "@/contexts/UiPreferencesContext"; import { ConnectionDefaultsProvider } from "@/contexts/ConnectionDefaultsContext"; import { BrandingProvider } from "@/contexts/BrandingContext"; -import { startPluginRuntime } from "@/plugin-host/loader"; +import { fetchGuestViews, startPluginRuntime } from "@/plugin-host/loader"; import { standaloneViewFor } from "@/shell/tab-registry"; import { PluginViewPlaceholder } from "@/plugin-host/PluginViewPlaceholder"; @@ -46,14 +46,6 @@ const ElectronVersionCheck = lazy(() => })), ); -// Anonymous guest view for shared terminal/RDP/VNC/Telnet sessions (?view=shared&token=). -// Rendered outside FullscreenAppGate since guests never have a JWT/cookie to verify. -const SharedSessionView = lazy( - () => import("@/features/session-sharing/SharedSessionView"), -); -// Anonymous guest view for collab rooms (?view=collab-guest&token=). -const CollabGuestView = lazy(() => import("@/features/collab/CollabGuestView")); - type Phase = "verifying" | "idle-auth" | "fading-in" | "idle-app" | "fading-out"; @@ -375,17 +367,49 @@ function App() { } /** - * Guest links draw surfaces plugins provide (a remote desktop stream), so the - * guest-capable plugins load first, without a session. + * An anonymous guest link (a shared session, a collab room). The view comes + * from a guest plugin's standalone tab view, so the guest-capable plugins + * load first, without a session. */ -function GuestPlugins({ children }: { children: React.ReactNode }) { +function GuestView({ view }: { view: string }) { const [ready, setReady] = useState(false); useEffect(() => { startPluginRuntime({ guest: true }) .catch(() => {}) .finally(() => setReady(true)); }, []); - return ready ? <>{children} : null; + if (!ready) return null; + const def = standaloneViewFor(view); + if (!def?.standalone) { + return ; + } + const Standalone = def.standalone; + return ( + + ); +} + +/** + * Picks between a guest link and a signed-in full-screen view. Guests never + * have a JWT or cookie to verify, so a guest view skips FullscreenAppGate. + */ +function ViewRouter({ view }: { view: string }) { + const [guestViews, setGuestViews] = useState(null); + useEffect(() => { + void fetchGuestViews().then(setGuestViews); + }, []); + if (!guestViews) return null; + if (guestViews.includes(view)) return ; + return ( + + + + + + ); } function RootApp() { @@ -394,37 +418,12 @@ function RootApp() { useServiceWorker(); const searchParams = new URLSearchParams(window.location.search); - const isFullscreen = searchParams.has("view"); + const view = searchParams.get("view"); - // Anonymous guests have no cookie/JWT at all, so this bypasses FullscreenAppGate's - // auth check entirely rather than waiting on a getUserInfo() call that would always fail. - if (searchParams.get("view") === "shared") { + if (view !== null) { return ( - - - - - ); - } - if (searchParams.get("view") === "collab-guest") { - return ( - - - - - - ); - } - - if (isFullscreen) { - return ( - - - - - - + ); } diff --git a/src/types/index.ts b/src/types/index.ts index 46e28de04..2c927be6f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -222,7 +222,6 @@ export type Host = { enableTmuxMonitor: boolean; enableTerminalToolbar: boolean; enableAiAssistant: boolean; - allowSessionSharing?: boolean; showTerminalInSidebar: boolean; showFileManagerInSidebar: boolean; showTunnelInSidebar: boolean; @@ -384,7 +383,6 @@ export interface HostData { enableTmuxMonitor?: boolean; enableTerminalToolbar?: boolean; enableAiAssistant?: boolean; - allowSessionSharing?: boolean; proxmoxConfig?: ProxmoxConfig | Record | null; enableProxmoxStats?: boolean; proxmoxStatsConfig?: ProxmoxStatsConfig | Record | null; diff --git a/src/types/ui-types.ts b/src/types/ui-types.ts index 43824bcd3..0d166d3ee 100644 --- a/src/types/ui-types.ts +++ b/src/types/ui-types.ts @@ -58,7 +58,6 @@ export type Host = { enableTerminal: boolean; enableCommandHistory: boolean; enableSessionLogging?: boolean; - allowSessionSharing?: boolean; /** Stable identity across a desktop/server sync pair. */ syncId?: string | null; terminalConfig?: Partial; @@ -279,7 +278,6 @@ export type KnownTabType = | "ssh-tools" | "automations" | "ai" - | "collab" | "split-screen"; /** @@ -309,8 +307,6 @@ export type Tab = { initialPath?: string; /** Payload owned by the tab's plugin, e.g. which fleet or endpoint it shows. */ data?: Record; - /** Which collab room a collab tab is showing. */ - collabRoomId?: string; /** Present only on a split-screen container tab. Pane ids reference live child tabs. */ splitConfig?: SplitTabConfig; /** Hides this session from the top-level tab bar while it belongs to a split tab. */ @@ -326,15 +322,7 @@ export type Tab = { notifyResize?: () => void; refresh?: () => void; getApplicationCursorKeysMode?: () => boolean; - openShareModal?: () => void; - canShare?: () => boolean; openFileManager?: () => void; - getShareTarget?: () => { - hostId: number; - sessionId: string; - protocol: "ssh"; - tabInstanceId?: string; - } | null; focus?: () => void; } | null>; }; diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx index f2522df50..c0804f124 100644 --- a/src/ui/AppShell.tsx +++ b/src/ui/AppShell.tsx @@ -30,6 +30,7 @@ import { useIsMobile } from "@/hooks/use-mobile"; import { resetPermissionsCache } from "@/hooks/use-permissions"; import { MobileBottomBar } from "@/shell/MobileBottomBar"; import { AppRail, type RailView } from "@/sidebar/AppRail"; +import { ComponentSlot } from "@/shell/ActionSlot"; import { isCoreRailView, railItemLabel, @@ -81,27 +82,10 @@ const AlertManager = lazy(() => const SshToolsPanel = lazy(() => import("@/sidebar/SshToolsPanel").then((m) => ({ default: m.SshToolsPanel })), ); -const CollabPanel = lazy(() => - import("@/sidebar/CollabPanel").then((m) => ({ default: m.CollabPanel })), -); const MacrosPanel = lazy(() => import("@/sidebar/MacrosPanel").then((m) => ({ default: m.MacrosPanel })), ); -const ShareSessionModal = lazy(() => - import("@/features/session-sharing/ShareSessionModal").then((m) => ({ - default: m.ShareSessionModal, - })), -); - -/** What a session tab's handle gives the shell's share dialog. */ -interface ShareTarget { - hostId: number; - sessionId: string; - protocol: "ssh"; - tabInstanceId?: string; -} - const SessionLogsPanel = lazy(() => import("@/sidebar/SessionLogsPanel").then((m) => ({ default: m.SessionLogsPanel, @@ -614,7 +598,6 @@ export function AppShell({ // Once those panels can themselves be the active tab, activeTabId points at // the panel and the lookup misses, so remember the last terminal instead. const [lastTerminalTabId, setLastTerminalTabId] = useState(activeTabId); - const [shareTarget, setShareTarget] = useState(null); useEffect(() => { const active = tabs.find((t) => t.id === activeTabId); if (active && getTabType(active.type)?.commandTarget) { @@ -1554,7 +1537,6 @@ export function AppShell({ initialPath?: string; joinSharedSessionId?: string | null; joinShareId?: string | null; - collabRoomId?: string; }, options?: { data?: Record; @@ -1616,7 +1598,6 @@ export function AppShell({ joinShareId, initialFilePath, initialPath, - collabRoomId: restore?.collabRoomId, }, ]; } @@ -1654,7 +1635,6 @@ export function AppShell({ joinShareId, initialFilePath, initialPath, - collabRoomId: restore?.collabRoomId, data: options?.data, }, ]; @@ -1729,52 +1709,6 @@ export function AppShell({ return id; } - // Invite awareness: rooms are discovered by polling, so a room that has - // never been shown to this browser gets one toast with an Open action. - useEffect(() => { - const SEEN_KEY = "termix:collab-rooms-seen"; - let cancelled = false; - const check = async () => { - try { - const { listCollabRooms } = await import("@/api/collab-api"); - const { rooms = [] } = await listCollabRooms(); - if (cancelled) return; - let seen: string[] = []; - try { - seen = JSON.parse(localStorage.getItem(SEEN_KEY) ?? "[]"); - } catch { - seen = []; - } - const seenSet = new Set(seen); - const fresh = rooms.filter((room) => !seenSet.has(room.id)); - if (fresh.length === 0) return; - localStorage.setItem( - SEEN_KEY, - JSON.stringify([...seenSet, ...fresh.map((room) => room.id)]), - ); - // The first poll after login only records what already exists. - if (seen.length === 0) return; - for (const room of fresh) { - if (room.ownerUserId === userId) continue; - toast(t("collab.invitedTo", { name: room.name }), { - action: { - label: t("collab.openRoom"), - onClick: () => setRailView("collab"), - }, - }); - } - } catch { - /* next poll */ - } - }; - void check(); - const timer = setInterval(() => void check(), 60_000); - return () => { - cancelled = true; - clearInterval(timer); - }; - }, []); - const openSingletonTab = useCallback( // `host` optionally preselects a host for a singleton plugin tab. function openSingletonTab( @@ -1962,24 +1896,6 @@ export function AppShell({ else handle.refresh?.(); } - function openShareForTab(id: string) { - const tab = tabs.find((t) => t.id === id); - if (!tab) return; - const ref = tab.terminalRef?.current as TabHandle | null | undefined; - // A session tab either opens its own share dialog or hands over what - // the shell's dialog needs. - const target = ( - ref?.getShareTarget as (() => ShareTarget | null) | undefined - )?.(); - if (target) { - setShareTarget(target); - } else if (ref?.canShare?.()) { - ref.openShareModal?.(); - } else { - toast.error(t("sessionSharing.notReadyToShare")); - } - } - function closeTab(id: string) { const tab = tabs.find((t) => t.id === id); const confirmEnabled = localStorage.getItem("confirmTabClose") === "true"; @@ -2609,58 +2525,6 @@ export function AppShell({
)} - {railView === "collab" && ( -
- { - const roomHost: Host = { - id: `collab-${room.id}`, - name: room.name, - username: "", - ip: "", - port: 0, - folder: "", - online: false, - cpu: null, - ram: null, - lastAccess: new Date().toISOString(), - authType: "none", - enableTerminal: false, - enableCommandHistory: false, - enableTunnel: false, - enableFileManager: false, - enableDocker: false, - enableProxmox: false, - enableProxmoxStats: false, - enableTmuxMonitor: false, - enableTerminalToolbar: false, - enableAiAssistant: false, - enableSsh: false, - enableRdp: false, - enableVnc: false, - enableTelnet: false, - sshPort: 22, - rdpPort: 3389, - vncPort: 5900, - telnetPort: 23, - serverTunnels: [], - quickActions: [], - }; - openTab(roomHost, "collab", { - instanceId: - typeof crypto.randomUUID === "function" - ? crypto.randomUUID() - : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`, - restoredSessionId: null, - savedLabel: room.name, - collabRoomId: room.id, - }); - if (isMobile) setSidebarOpen(false); - }} - /> -
- )} - {railView === "session-logs" && (
@@ -2921,7 +2785,6 @@ export function AppShell({ void invokeAction("files.openHost", targetTab.host); } }} - onOpenShare={openShareForTab} isAppFullscreen={isAppFullscreen} onToggleAppFullscreen={toggleAppFullscreen} rightDockOpen={rightRailView !== null} @@ -3046,18 +2909,7 @@ export function AppShell({
- {shareTarget && ( - - setShareTarget(null)} - hostId={shareTarget.hostId} - sessionId={shareTarget.sessionId} - protocol={shareTarget.protocol} - tabInstanceId={shareTarget.tabInstanceId} - /> - - )} + {commandPaletteOpen && ( diff --git a/src/ui/api/collab-api.ts b/src/ui/api/collab-api.ts deleted file mode 100644 index 9661e1145..000000000 --- a/src/ui/api/collab-api.ts +++ /dev/null @@ -1,240 +0,0 @@ -import axios from "axios"; -import { authApi, handleApiError } from "@/main-axios"; -import { resolveApiBaseUrl } from "@/api/session-sharing-api"; - -export interface CollabRoom { - id: string; - name: string; - ownerUserId: string; - persistent: boolean; - presenterUserId: string | null; - stageProtocol: string | null; - stageHostId: number | null; - stageShareId: string | null; - guestLinkEnabled: boolean; - createdAt: string; - endedAt: string | null; -} - -export interface CollabRoomMember { - userId: string; - username: string; - roomRole: string; - createdAt: string; -} - -export interface CollabOnlineUser { - userId: string; - username: string; -} - -export interface CollabStage { - presenterUserId: string | null; - protocol: "ssh" | "rdp" | "vnc" | "telnet" | null; - hostId: number | null; - shareId: string | null; - sessionId?: string; - controllerUserId?: string | null; - connectParams?: { token: string }; -} - -export interface CollabRoomDetail { - room: CollabRoom; - me: string; - isHost: boolean; - members: CollabRoomMember[]; - online: CollabOnlineUser[]; - stage: CollabStage; - controllerUserId: string | null; - controlRequests: CollabControlRequest[]; - /** Where live room events are served, as /plugin-ws//. */ - eventsWsPath: string; -} - -export interface CollabControlRequest { - userId: string; - username: string; - requestedAt: string; -} - -export async function listCollabRooms(): Promise<{ rooms: CollabRoom[] }> { - try { - const response = await authApi.get("/collab/rooms"); - return response.data; - } catch (error) { - throw handleApiError(error, "list collab rooms"); - } -} - -export async function createCollabRoom( - name: string, - persistent: boolean, -): Promise<{ room: CollabRoom }> { - try { - const response = await authApi.post("/collab/rooms", { name, persistent }); - return response.data; - } catch (error) { - throw handleApiError(error, "create collab room"); - } -} - -export async function getCollabRoom(roomId: string): Promise { - try { - const response = await authApi.get(`/collab/rooms/${roomId}`); - return response.data; - } catch (error) { - throw handleApiError(error, "get collab room"); - } -} - -export async function inviteCollabMembers( - roomId: string, - targets: { userIds?: string[]; roleIds?: number[] }, -): Promise { - try { - await authApi.post(`/collab/rooms/${roomId}/members`, targets); - } catch (error) { - throw handleApiError(error, "invite collab members"); - } -} - -export async function removeCollabMember( - roomId: string, - userId: string, -): Promise { - try { - await authApi.delete(`/collab/rooms/${roomId}/members/${userId}`); - } catch (error) { - throw handleApiError(error, "remove collab member"); - } -} - -export async function presentCollabStage( - roomId: string, - input: { protocol: string; sessionId: string; hostId: number }, -): Promise<{ stage: CollabStage }> { - try { - const response = await authApi.post( - `/collab/rooms/${roomId}/present`, - input, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "start presenting"); - } -} - -export async function stopCollabStage(roomId: string): Promise { - try { - await authApi.post(`/collab/rooms/${roomId}/stop`); - } catch (error) { - throw handleApiError(error, "stop presenting"); - } -} - -export async function getCollabStage( - roomId: string, -): Promise<{ stage: CollabStage | null }> { - try { - const response = await authApi.get(`/collab/rooms/${roomId}/stage`); - return response.data; - } catch (error) { - throw handleApiError(error, "resolve collab stage"); - } -} - -export async function setCollabStageControl( - roomId: string, - userId: string | null, -): Promise { - try { - await authApi.post(`/collab/rooms/${roomId}/control`, { userId }); - } catch (error) { - throw handleApiError(error, "change stage control"); - } -} - -export async function requestCollabStageControl( - roomId: string, -): Promise<{ request: CollabControlRequest }> { - try { - const response = await authApi.post( - `/collab/rooms/${roomId}/control/request`, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "request stage control"); - } -} - -export async function listCollabControlRequests( - roomId: string, -): Promise<{ requests: CollabControlRequest[] }> { - try { - const response = await authApi.get( - `/collab/rooms/${roomId}/control/requests`, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "list control requests"); - } -} - -export async function dismissCollabControlRequest( - roomId: string, - userId: string, -): Promise { - try { - await authApi.delete(`/collab/rooms/${roomId}/control/requests/${userId}`); - } catch (error) { - throw handleApiError(error, "dismiss control request"); - } -} - -export async function endCollabRoom(roomId: string): Promise { - try { - await authApi.post(`/collab/rooms/${roomId}/end`); - } catch (error) { - throw handleApiError(error, "end collab room"); - } -} - -export async function deleteCollabRoom(roomId: string): Promise { - try { - await authApi.delete(`/collab/rooms/${roomId}`); - } catch (error) { - throw handleApiError(error, "delete collab room"); - } -} - -export async function setCollabGuestLink( - roomId: string, - enabled: boolean, -): Promise<{ guestLinkToken: string | null }> { - try { - const response = await authApi.post(`/collab/rooms/${roomId}/guest-link`, { - enabled, - }); - return response.data; - } catch (error) { - throw handleApiError(error, "update guest link"); - } -} - -export interface CollabGuestStage { - protocol: "ssh" | "rdp" | "vnc" | "telnet"; - shareId: string; - wsPath?: string; - connectParams?: { token: string }; -} - -/** Anonymous: guests poll this to follow the presenter. Throws on 404/429. */ -export async function resolveCollabGuestStage( - token: string, -): Promise<{ roomName: string; stage: CollabGuestStage | null }> { - const baseUrl = await resolveApiBaseUrl(); - const response = await axios.get( - `${baseUrl}/collab/guest/${encodeURIComponent(token)}`, - ); - return response.data; -} diff --git a/src/ui/api/open-tabs-api.ts b/src/ui/api/open-tabs-api.ts index 9a39a21fe..d78162a2a 100644 --- a/src/ui/api/open-tabs-api.ts +++ b/src/ui/api/open-tabs-api.ts @@ -2,6 +2,9 @@ import { authApi } from "@/main-axios"; import { createTtlRequestCache } from "@/lib/ttl-request-cache"; import type { TerminalTheme } from "@/lib/terminal-themes"; import type { CustomKeybinding } from "@/types/keybindings"; +import { invokeAction, isActionRegistered } from "@/shell/action-registry"; + +const SHARED_WITH_ME_ACTION = "sessions.sharedWithMe"; // OPEN TABS API // ============================================================================ @@ -84,10 +87,21 @@ export async function addOpenTab(tab: OpenTabUpsertPayload): Promise { await authApi.post("/open-tabs", tab); } +/** + * The caller's live sessions, plus sessions other users shared with them, + * which the session sharing plugin answers through an action (none while it + * is off). + */ export async function getActiveSessions(): Promise { return activeSessionsCache.get(async () => { - const response = await authApi.get("/open-tabs/active-sessions"); - return Array.isArray(response.data) ? response.data : []; + const [response, shared] = await Promise.all([ + authApi.get("/open-tabs/active-sessions"), + isActionRegistered(SHARED_WITH_ME_ACTION) + ? invokeAction(SHARED_WITH_ME_ACTION).catch(() => []) + : Promise.resolve([]), + ]); + const own = Array.isArray(response.data) ? response.data : []; + return Array.isArray(shared) ? [...own, ...shared] : own; }); } diff --git a/src/ui/api/session-sharing-api.ts b/src/ui/api/session-sharing-api.ts deleted file mode 100644 index 42a5a9728..000000000 --- a/src/ui/api/session-sharing-api.ts +++ /dev/null @@ -1,181 +0,0 @@ -import axios from "axios"; -import { getBasePath } from "@/lib/base-path"; -import { isElectron } from "@/lib/electron"; -import { authApi, handleApiError } from "@/main-axios"; - -export interface ResolvedShareLink { - protocol: "ssh" | "rdp" | "vnc" | "telnet"; - permissionLevel: "read-only" | "read-write"; - wsPath: string; - connectParams?: { token: string }; -} - -export type ShareLinkErrorKind = "not-found" | "rate-limited" | "unknown"; - -export class ShareLinkError extends Error { - constructor( - message: string, - public readonly kind: ShareLinkErrorKind, - ) { - super(message); - this.name = "ShareLinkError"; - } -} - -const isDev = (): boolean => - !isElectron() && - process.env.NODE_ENV === "development" && - (window.location.port === "3000" || - window.location.port === "5173" || - window.location.port === ""); - -// Guests have no session/JWT, so this deliberately builds a bare base URL -// rather than going through main-axios's authenticated instances. The -// desktop app always runs its embedded local backend as the source of -// truth, so a share link opened there always resolves against it -- -// joining a session hosted on someone else's remote server isn't -// supported from the desktop app today. -export async function resolveApiBaseUrl(): Promise { - if (isDev()) { - const protocol = window.location.protocol === "https:" ? "https" : "http"; - return `${protocol}://localhost:30001`; - } - if (isElectron()) { - return "http://127.0.0.1:30001"; - } - return getBasePath(); -} - -export async function resolveShareLink( - linkToken: string, -): Promise { - const baseUrl = await resolveApiBaseUrl(); - try { - const response = await axios.get( - `${baseUrl}/session-sharing/resolve/${encodeURIComponent(linkToken)}`, - ); - return response.data; - } catch (error) { - if (axios.isAxiosError(error)) { - if (error.response?.status === 404) { - throw new ShareLinkError( - "Share link is invalid, expired, or revoked", - "not-found", - ); - } - if (error.response?.status === 429) { - throw new ShareLinkError( - "Too many attempts, please try again shortly", - "rate-limited", - ); - } - } - throw new ShareLinkError("Failed to resolve share link", "unknown"); - } -} - -// ============================================================================ -// SESSION SHARING (authenticated owner-side API) -// ============================================================================ - -export type SessionShareProtocol = "ssh" | "rdp" | "vnc" | "telnet"; -export type SessionShareType = "link" | "user"; -export type SessionSharePermissionLevel = "read-only" | "read-write"; - -export interface SessionShareRecord { - id: string; - hostId: number; - ownerUserId: string; - protocol: SessionShareProtocol; - sessionId: string; - tabInstanceId: string | null; - shareType: SessionShareType; - targetUserId: string | null; - linkToken: string | null; - permissionLevel: SessionSharePermissionLevel; - createdAt: string; - expiresAt: string; - revokedAt: string | null; - lastJoinedAt: string | null; - joinCount: number; -} - -export interface CreateSessionShareRequest { - hostId: number; - sessionId: string; - tabInstanceId?: string; - protocol: SessionShareProtocol; - shareType: SessionShareType; - targetUserId?: string; - permissionLevel: SessionSharePermissionLevel; - expiryHours?: number; -} - -export interface CreateSessionShareResponse { - shareId: string; - linkToken: string | null; - expiresAt: string; -} - -export async function createSessionShare( - request: CreateSessionShareRequest, -): Promise { - try { - const response = await authApi.post("/session-sharing/create", request); - return response.data; - } catch (error) { - throw handleApiError(error, "create session share"); - } -} - -export async function getActiveSessionShares( - hostId: number, -): Promise<{ shares: SessionShareRecord[] }> { - try { - const response = await authApi.get( - `/session-sharing/host/${hostId}/active`, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "fetch active session shares"); - } -} - -export async function revokeSessionShare( - shareId: string, -): Promise<{ success: true }> { - try { - const response = await authApi.delete(`/session-sharing/${shareId}`); - return response.data; - } catch (error) { - throw handleApiError(error, "revoke session share"); - } -} - -// ============================================================================ -// GLOBAL ADMIN TOGGLE -// ============================================================================ - -export async function getSessionSharingGloballyEnabled(): Promise<{ - enabled: boolean; -}> { - try { - const response = await authApi.get("/users/session-sharing-enabled"); - return response.data; - } catch (error) { - throw handleApiError(error, "fetch session sharing enabled setting"); - } -} - -export async function updateSessionSharingGloballyEnabled( - enabled: boolean, -): Promise<{ enabled: boolean }> { - try { - const response = await authApi.patch("/users/session-sharing-enabled", { - enabled, - }); - return response.data; - } catch (error) { - throw handleApiError(error, "update session sharing enabled setting"); - } -} diff --git a/src/ui/features/remote-display/RemoteDisplay.tsx b/src/ui/features/remote-display/RemoteDisplay.tsx deleted file mode 100644 index c1fa32e0e..000000000 --- a/src/ui/features/remote-display/RemoteDisplay.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useMemo } from "react"; -import { useActionSlot } from "@/hooks/use-action-slot"; -import { invokeAction, isActionRegistered } from "@/shell/action-registry"; -import { PluginViewPlaceholder } from "@/plugin-host/PluginViewPlaceholder"; - -/** - * A remote desktop session drawn by whichever plugin provides one. - * - * Collab rooms and shared-session links show RDP, VNC and Telnet streams - * without knowing how they are drawn. The provider contributes a component - * to this slot and handles the token action below. - */ -export const REMOTE_DISPLAY_SLOT = "session.remoteDisplay"; -export const REMOTE_SESSION_TOKEN_ACTION = "session.remoteDisplay.token"; - -export interface RemoteDisplayProps { - token: string; - protocol: string; - isVisible: boolean; - onConnect?: () => void; - onError?: (error: string) => void; -} - -export function RemoteDisplay(props: RemoteDisplayProps) { - const contributions = useActionSlot(REMOTE_DISPLAY_SLOT); - const provider = useMemo( - () => - contributions.find( - (contribution) => - contribution.kind === "component" && contribution.component, - ), - [contributions], - ); - if (!provider?.component) { - return ; - } - const Display = provider.component; - return )} />; -} - -export interface RemoteSessionToken { - token: string; - connectionId: string; -} - -/** Mints a session token for presenting a host, through the provider. */ -export async function createRemoteSessionToken( - hostId: number, - origin: unknown, - protocol: string, -): Promise { - if (!isActionRegistered(REMOTE_SESSION_TOKEN_ACTION)) return null; - return (await invokeAction( - REMOTE_SESSION_TOKEN_ACTION, - hostId, - origin, - protocol, - )) as RemoteSessionToken | null; -} diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index 30e40b8f6..ef3010620 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -664,7 +664,6 @@ "done_cli_link": "Open the CLI docs" }, "nav": { - "collab": "Meetings", "home": "Home", "terminal": "Terminal", "localTerminal": "Local Terminal", @@ -1032,8 +1031,6 @@ "enableAutoTmuxDesc": "Keep your shell and anything running in it alive across disconnects and browser restarts by running inside tmux (tmux must be installed on the host)", "enableSessionLogging": "Session Logging", "enableSessionLoggingDesc": "Record terminal session output for later review", - "allowSessionSharing": "Allow Session Sharing", - "allowSessionSharingDesc": "Let live sessions on this host be shared via link or with other users", "localEchoLabel": "Local Echo", "localEchoDesc": "Predict safe printable keystrokes locally on slow connections. Password prompts and control input are never echoed.", "localEchoDefault": "Use global setting", @@ -1573,137 +1570,11 @@ "join": "Join", "sharedSessionLabel": "{{hostName}} (shared)" }, - "collab": { - "createRoom": "New room", - "roomName": "Room name", - "persistentRoom": "Persistent room", - "persistentRoomHint": "Persistent rooms stay listed after the meeting ends and can be reused.", - "noRooms": "No rooms yet. Create one to start a meeting.", - "openRoom": "Open", - "endRoom": "End meeting", - "leaveRoom": "Leave", - "deleteMember": "Remove", - "removeMember": "Remove {{name}}", - "removeMemberTitle": "Remove this member?", - "removeMemberDescription": "{{name}} will lose access to this room immediately. Any active SSH viewer will be disconnected.", - "invite": "Invite", - "inviteTitle": "Invite members", - "members": "Members", - "membersPanel": "Room members and control requests", - "membersWithCount": "Members ({{count}})", - "online": "online", - "offline": "offline", - "you": "(you)", - "hostBadge": "Host", - "presenterBadge": "Presenting", - "present": "Present", - "presentTitle": "Take the stage", - "presentHost": "Host", - "searchHosts": "Search hosts", - "noHostsFound": "No available hosts found.", - "presentProtocol": "Protocol", - "stopPresenting": "Stop presenting", - "takeOver": "Take over", - "takeOverConfirmTitle": "Replace the current presentation?", - "takeOverDescription": "{{name}} is presenting now. Taking over will disconnect everyone from that stage.", - "emptyStage": "Nobody is presenting. Take the stage to share a session.", - "stageLoading": "Connecting to the stage...", - "stageEnded": "The presentation ended", - "roomEnded": "This meeting has ended", - "presenterLabel": "{{name}} is presenting", - "guestLink": "Guest link", - "guestLinkOn": "Guest link is on. Anyone with the link can watch the stage.", - "guestLinkOff": "Guest link is off.", - "copyLink": "Copy link", - "rotateLink": "Rotate link", - "disableLink": "Disable link", - "rotateLinkConfirmTitle": "Rotate the guest link?", - "rotateLinkDescription": "The current link will stop working immediately. Connected SSH guests will be disconnected.", - "disableLinkConfirmTitle": "Disable the guest link?", - "disableLinkDescription": "Anonymous SSH viewers will be disconnected immediately and the current link will stop working.", - "linkCopied": "Link copied", - "roles": "Roles", - "users": "Users", - "searchUsers": "Search users", - "endConfirmTitle": "End this meeting?", - "endDescription": "{{name}} will end for everyone and cannot be reopened.", - "endPersistentDescription": "The active presentation in {{name}} will stop. The room remains available for reuse.", - "deleteRoom": "Delete room", - "deleteConfirmTitle": "Delete this room?", - "deleteDescription": "{{name}} will be permanently deleted for everyone and cannot be undone.", - "invitedTo": "You were invited to \"{{name}}\"", - "guest": { - "title": "Meeting", - "waiting": "Waiting for the presenter...", - "linkInvalid": "This guest link is invalid or the meeting has ended" - }, - "reopenFromPanel": "This meeting tab expired. Reopen the room from the Meetings panel.", - "requestControl": "Request control", - "cancelControlRequest": "Cancel request", - "controlQueue": "Control requests", - "noControlRequests": "No pending requests.", - "grantControlTo": "Give control to {{name}}", - "dismissControlRequest": "Dismiss {{name}}'s control request", - "releaseControl": "Release control", - "grantControl": "Give control", - "revokeControl": "Take back control", - "controlRequestedBy": "{{name}} asked for control", - "grant": "Grant", - "controlBadge": "In control", - "youArePresenting": "You are presenting in another tab. Stop presenting to hand the stage over.", - "created": "Room created", - "invited": "Members invited", - "roomTab": "Meeting" - }, "sessionSharing": { - "guestView": { - "loading": "Connecting to shared session...", - "linkInvalid": "This share link is invalid, expired, or has been revoked", - "rateLimited": "Too many attempts, please try again shortly", - "sessionEnded": "This session has ended", - "readOnlyBadge": "View only", - "ownerLabel": "Host owner" - }, - "modalTitle": "Share session", - "shareButton": "Share", - "notReadyToShare": "Session is not ready to share yet", - "modeTab": { - "link": "Link", - "user": "User" - }, - "linkModeDescription": "Anyone with this link can join, no account required.", - "userModeDescription": "Share with a specific user who already has access to this host. If they do not have access yet, share the host with them first or use a link instead. Once shared, the session appears in their Connections tab.", "permissionLevel": { - "label": "Permission level", "readOnly": "Read-only", - "readOnlyDescription": "Can watch the session live but cannot type or interact.", - "readWrite": "Read-write", - "readWriteDescription": "Can type and interact with the session just like the owner." - }, - "expiryLabel": "Link expiry", - "createLinkButton": "Create link", - "createShareButton": "Share with user", - "searchUsersPlaceholder": "Search users...", - "noUsersFound": "No users found", - "linkCreated": "Share link created", - "linkCopied": "Link copied to clipboard", - "copyLink": "Copy link", - "shareCreated": "Session shared. It will appear in their Connections tab.", - "shareFailed": "Failed to create share", - "userLacksHostAccess": "That user does not have access to this host yet. Share the host with them first, or use a link instead.", - "activeShares": "Active shares", - "noActiveShares": "No active shares for this session", - "revoke": "Revoke", - "revokeConfirmTitle": "Revoke this share?", - "revokeConfirmDescription": "Anyone using this share will lose access immediately.", - "revoked": "Share revoked", - "revokeFailed": "Failed to revoke share", - "joinCount": "{{count}} join", - "joinCount_other": "{{count}} joins", - "expiresAt": "Expires {{date}}", - "linkShareBadge": "Link", - "userShareBadge": "User: {{username}}", - "loadSharesFailed": "Failed to load active shares" + "readWrite": "Read-write" + } }, "terminal": { "connect": "Connect to Host", @@ -2251,7 +2122,6 @@ "analyticsEnabledDesc": "Sends an anonymous daily count of users, hosts, and feature usage to help improve Termix. No personal data or connection details are ever included.", "analyticsEnabledLockedDesc": "This setting is locked by the ENABLE_TELEMETRY environment variable and cannot be changed here.", "updateAnalyticsFailed": "Failed to update analytics setting", - "sessionSharingGloballyEnabled": "Allow Session Sharing", "auditForwardingTitle": "Forward to SIEM", "auditForwardingDesc": "Ship a copy of every audit entry to an external collector as NDJSON. Local logs stay the source of truth.", "auditForwardingEnvNotice": "An environment variable also configures forwarding; the URL saved here takes precedence.", @@ -2259,8 +2129,6 @@ "auditForwardingTokenStored": "Bearer token (stored)", "auditForwardingSaved": "Audit forwarding settings saved", "auditForwardingSaveError": "Failed to save audit forwarding settings", - "sessionSharingGloballyEnabledDesc": "Allow live terminal, RDP, VNC, and Telnet sessions to be shared instance-wide. Overrides every per-host sharing toggle when disabled.", - "updateSessionSharingFailed": "Failed to update session sharing setting", "sessionTimeout": "Session Timeout", "hours": "hours", "sessionTimeoutRange": "Min 1h · Max 720h", diff --git a/src/ui/locales/translated/af_ZA.json b/src/ui/locales/translated/af_ZA.json index 20e87c1e5..acd30d0ca 100644 --- a/src/ui/locales/translated/af_ZA.json +++ b/src/ui/locales/translated/af_ZA.json @@ -667,7 +667,6 @@ "done_cli_link": "Maak die CLI-dokumente oop" }, "nav": { - "collab": "Vergaderings", "home": "Tuis", "terminal": "Terminaal", "localTerminal": "Plaaslike Terminaal", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Hou jou dop en enigiets wat daarin loop lewendig oor ontkoppelings en herbegin van blaaiers deur dit binne tmux te laat loop (tmux moet op die gasheer geïnstalleer wees)", "enableSessionLogging": "Sessie-logging", "enableSessionLoggingDesc": "Neem terminaalsessie-uitvoer op vir latere hersiening", - "allowSessionSharing": "Laat Sessiedeling Toe", - "allowSessionSharingDesc": "Laat regstreekse sessies op hierdie gasheer via skakel of met ander gebruikers gedeel word", "localEchoLabel": "Plaaslike Eggo", "localEchoDesc": "Voorspel veilige drukbare toetsaanslagen plaaslik op stadige verbindings. Wagwoordaanwysings en beheerinvoer word nooit herhaal nie.", "localEchoDefault": "Gebruik globale instelling", @@ -1571,137 +1568,11 @@ "join": "Sluit aan", "sharedSessionLabel": "{{hostName}} (gedeel)" }, - "collab": { - "createRoom": "Nuwe kamer", - "roomName": "Kamernaam", - "persistentRoom": "Permanente kamer", - "persistentRoomHint": "Permanente kamers bly gelys nadat die vergadering eindig en kan hergebruik word.", - "noRooms": "Geen kamers nog nie. Skep een om 'n vergadering te begin.", - "openRoom": "Oop", - "endRoom": "Beëindig vergadering", - "leaveRoom": "Vertrek", - "deleteMember": "Verwyder", - "removeMember": "Verwyder {{name}}", - "removeMemberTitle": "Verwyder hierdie lid?", - "removeMemberDescription": "{{name}} sal onmiddellik toegang tot hierdie kamer verloor. Enige aktiewe SSH-kyker sal ontkoppel word.", - "invite": "Nooi uit", - "inviteTitle": "Nooi lede uit", - "members": "Lede", - "membersPanel": "Kamerlede en beheerversoeke", - "membersWithCount": "Lede ({{count}})", - "online": "aanlyn", - "offline": "vanlyn", - "you": "(jy)", - "hostBadge": "Gasheer", - "presenterBadge": "Aanbieding", - "present": "Aanwesig", - "presentTitle": "Neem die verhoog", - "presentHost": "Gasheer", - "searchHosts": "Soek gashere", - "noHostsFound": "Geen beskikbare gashere gevind nie.", - "presentProtocol": "Protokol", - "stopPresenting": "Hou op met aanbied", - "takeOver": "Neem oor", - "takeOverConfirmTitle": "Vervang die huidige aanbieding?", - "takeOverDescription": "{{name}} bied nou aan. Oorname sal almal van daardie stadium ontkoppel.", - "emptyStage": "Niemand bied aan nie. Neem die verhoog om 'n sessie te deel.", - "stageLoading": "Verbind met die verhoog...", - "stageEnded": "Die aanbieding het geëindig", - "roomEnded": "Hierdie vergadering het geëindig", - "presenterLabel": "{{name}} bied aan", - "guestLink": "Gas skakel", - "guestLinkOn": "Gasskakel is aan. Enigiemand met die skakel kan die verhoog kyk.", - "guestLinkOff": "Gas skakel is af.", - "copyLink": "Kopieer skakel", - "rotateLink": "Roteer skakel", - "disableLink": "Deaktiveer skakel", - "rotateLinkConfirmTitle": "Draai die gasskakel?", - "rotateLinkDescription": "Die huidige skakel sal onmiddellik ophou werk. Gekoppelde SSH-gaste sal ontkoppel word.", - "disableLinkConfirmTitle": "Deaktiveer die gasskakel?", - "disableLinkDescription": "Anonieme SSH-kykers sal onmiddellik ontkoppel word en die huidige skakel sal ophou werk.", - "linkCopied": "Skakel gekopieer", - "roles": "Rolle", - "users": "Gebruikers", - "searchUsers": "Soek gebruikers", - "endConfirmTitle": "Beëindig hierdie vergadering?", - "endDescription": "{{name}} sal vir almal eindig en kan nie weer heropen word nie.", - "endPersistentDescription": "Die aktiewe aanbieding in {{name}} sal stop. Die kamer bly beskikbaar vir hergebruik.", - "deleteRoom": "Vee kamer uit", - "deleteConfirmTitle": "Vee hierdie kamer uit?", - "deleteDescription": "{{name}} sal permanent vir almal uitgevee word en kan nie ongedaan gemaak word nie.", - "invitedTo": "Jy is genooi na \"{{name}}\"", - "guest": { - "title": "Vergadering", - "waiting": "Wag vir die aanbieder...", - "linkInvalid": "Hierdie gasskakel is ongeldig of die vergadering het geëindig" - }, - "reopenFromPanel": "Hierdie vergadering-oortjie het verval. Maak die kamer weer oop vanaf die Vergaderings-paneel.", - "requestControl": "Versoekbeheer", - "cancelControlRequest": "Kanselleer versoek", - "controlQueue": "Beheerversoeke", - "noControlRequests": "Geen hangende versoeke nie.", - "grantControlTo": "Gee beheer aan {{name}}", - "dismissControlRequest": "Maak {{name}}se beheerversoek toe", - "releaseControl": "Vrystellingsbeheer", - "grantControl": "Gee beheer", - "revokeControl": "Neem beheer terug", - "controlRequestedBy": "{{name}} het om beheer gevra", - "grant": "Toekenning", - "controlBadge": "In beheer", - "youArePresenting": "Jy bied aan in 'n ander oortjie. Stop die aanbieding om die verhoog oor te dra.", - "created": "Kamer geskep", - "invited": "Lede genooi", - "roomTab": "Vergadering" - }, "sessionSharing": { - "guestView": { - "loading": "Verbind met gedeelde sessie...", - "linkInvalid": "Hierdie deelskakel is ongeldig, het verval of is herroep", - "rateLimited": "Te veel pogings, probeer asseblief weer binnekort", - "sessionEnded": "Hierdie sessie is verby", - "readOnlyBadge": "Slegs kyk", - "ownerLabel": "Gasheer-eienaar" - }, - "modalTitle": "Deel sessie", - "shareButton": "Deel", - "notReadyToShare": "Sessie is nog nie gereed om te deel nie", - "modeTab": { - "link": "Skakel", - "user": "Gebruiker" - }, - "linkModeDescription": "Enigiemand met hierdie skakel kan aansluit, geen rekening nodig nie.", - "userModeDescription": "Deel met 'n spesifieke gebruiker wat reeds toegang tot hierdie gasheer het. Indien hulle nog nie toegang het nie, deel die gasheer eers met hulle of gebruik eerder 'n skakel. Sodra dit gedeel is, verskyn die sessie in hul Verbindings-oortjie.", "permissionLevel": { - "label": "Toestemmingsvlak", "readOnly": "Leesalleen", - "readOnlyDescription": "Kan die sessie regstreeks kyk, maar kan nie tik of interaksie hê nie.", - "readWrite": "Lees-skryf", - "readWriteDescription": "Kan tik en met die sessie interaksie hê net soos die eienaar." - }, - "expiryLabel": "Skakelvervaldatum", - "createLinkButton": "Skep skakel", - "createShareButton": "Deel met gebruiker", - "searchUsersPlaceholder": "Soek gebruikers...", - "noUsersFound": "Geen gebruikers gevind nie", - "linkCreated": "Deel skakel geskep", - "linkCopied": "Skakel gekopieer na knipbord", - "copyLink": "Kopieer skakel", - "shareCreated": "Sessie gedeel. Dit sal in hul Verbindings-oortjie verskyn.", - "shareFailed": "Kon nie deel skep nie", - "userLacksHostAccess": "Daardie gebruiker het nog nie toegang tot hierdie gasheer nie. Deel die gasheer eers met hulle, of gebruik eerder 'n skakel.", - "activeShares": "Aktiewe aandele", - "noActiveShares": "Geen aktiewe delings vir hierdie sessie nie", - "revoke": "Herroep", - "revokeConfirmTitle": "Herroep hierdie deling?", - "revokeConfirmDescription": "Enigiemand wat hierdie deel gebruik, sal onmiddellik toegang verloor.", - "revoked": "Deel herroep", - "revokeFailed": "Kon nie deel herroep nie", - "joinCount": "{{count}} sluit aan", - "joinCount_other": "{{count}} sluit aan", - "expiresAt": "Vervaldatum {{date}}", - "linkShareBadge": "Skakel", - "userShareBadge": "Gebruiker: {{username}}", - "loadSharesFailed": "Kon nie aktiewe delings laai nie" + "readWrite": "Lees-skryf" + } }, "terminal": { "connect": "Koppel aan gasheer", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Stuur 'n anonieme daaglikse telling van gebruikers, gashere en funksiegebruik om Termix te help verbeter. Geen persoonlike data of verbindingsbesonderhede word ooit ingesluit nie.", "analyticsEnabledLockedDesc": "Hierdie instelling word gesluit deur die ENABLE_TELEMETRY omgewingveranderlike en kan nie hier verander word nie.", "updateAnalyticsFailed": "Kon nie analitiese instelling opdateer nie", - "sessionSharingGloballyEnabled": "Laat Sessiedeling Toe", "auditForwardingTitle": "Stuur aan na SIEM", "auditForwardingDesc": "Stuur 'n kopie van elke ouditinskrywing na 'n eksterne versamelaar as NDJSON. Plaaslike logboeke bly die bron van waarheid.", "auditForwardingEnvNotice": "'n Omgewingsveranderlike konfigureer ook aanstuur; die URL wat hier gestoor word, kry voorrang.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Draerteken (gestoor)", "auditForwardingSaved": "Oudit-aanstuurinstellings gestoor", "auditForwardingSaveError": "Kon nie instellings vir oudit-aanstuur stoor nie", - "sessionSharingGloballyEnabledDesc": "Laat lewendige terminaal-, RDP-, VNC- en Telnet-sessies toe om instansiewyd gedeel te word. Oorskryf elke deelskakelaar per gasheer wanneer dit gedeaktiveer is.", - "updateSessionSharingFailed": "Kon nie sessiedelinginstelling opdateer nie", "sessionTimeout": "Sessie-tydsbeperking", "hours": "ure", "sessionTimeoutRange": "Min 1 uur · Maks 720 uur", diff --git a/src/ui/locales/translated/ar_SA.json b/src/ui/locales/translated/ar_SA.json index 3a6e769b1..0ae5d45e5 100644 --- a/src/ui/locales/translated/ar_SA.json +++ b/src/ui/locales/translated/ar_SA.json @@ -667,7 +667,6 @@ "done_cli_link": "افتح وثائق واجهة سطر الأوامر" }, "nav": { - "collab": "الاجتماعات", "home": "الرئيسية", "terminal": "الطرفية", "localTerminal": "المحطة المحلية", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "حافظ على تشغيل shell وأي شيء يعمل بداخله أثناء انقطاع الاتصال وإعادة تشغيل المتصفح عن طريق التشغيل داخل tmux (يجب تثبيت tmux على المضيف).", "enableSessionLogging": "تسجيل الجلسات", "enableSessionLoggingDesc": "تسجيل مخرجات الجلسة الطرفية للمراجعة لاحقاً", - "allowSessionSharing": "السماح بمشاركة الجلسة", - "allowSessionSharingDesc": "يمكن مشاركة الجلسات المباشرة على هذا المضيف عبر رابط أو مع مستخدمين آخرين", "localEchoLabel": "صدى محلي", "localEchoDesc": "توقع ضغطات المفاتيح الآمنة القابلة للطباعة محليًا على الاتصالات البطيئة. لا يتم عرض مطالبات كلمة المرور أو مدخلات التحكم على الشاشة.", "localEchoDefault": "استخدم الإعدادات العامة", @@ -1571,137 +1568,11 @@ "join": "ينضم", "sharedSessionLabel": "{{hostName}} (مشترك)" }, - "collab": { - "createRoom": "غرف جديدة", - "roomName": "اسم الغرفة", - "persistentRoom": "غرفة مستمرة", - "persistentRoomHint": "تبقى الغرف الدائمة مدرجة بعد انتهاء الاجتماع ويمكن إعادة استخدامها.", - "noRooms": "لا توجد غرف اجتماعات بعد. أنشئ غرفة لبدء اجتماع.", - "openRoom": "يفتح", - "endRoom": "انتهاء الاجتماع", - "leaveRoom": "يترك", - "deleteMember": "يزيل", - "removeMember": "إزالة {{name}}", - "removeMemberTitle": "هل تريد حذف هذا العضو؟", - "removeMemberDescription": "سيفقد المستخدم {{name}} إمكانية الوصول إلى هذه الغرفة فورًا. وسيتم فصل أي برنامج عرض SSH نشط.", - "invite": "يدعو", - "inviteTitle": "دعوة الأعضاء", - "members": "أعضاء", - "membersPanel": "أعضاء الغرفة وطلبات التحكم", - "membersWithCount": "الأعضاء ({{count}})", - "online": "متصل", - "offline": "غير متصل بالإنترنت", - "you": "(أنت)", - "hostBadge": "يستضيف", - "presenterBadge": "تقديم", - "present": "حاضر", - "presentTitle": "اصعد إلى المسرح", - "presentHost": "يستضيف", - "searchHosts": "البحث عن المضيفين", - "noHostsFound": "لم يتم العثور على مضيفين متاحين.", - "presentProtocol": "بروتوكول", - "stopPresenting": "توقف عن التقديم", - "takeOver": "تولي زمام الأمور", - "takeOverConfirmTitle": "هل تريد استبدال العرض التقديمي الحالي؟", - "takeOverDescription": "{{name}} يقدم العرض الآن. سيؤدي الاستحواذ على المنصة إلى فصل الجميع عنها.", - "emptyStage": "لا يوجد مقدمون. تفضلوا بالصعود إلى المنصة لمشاركة جلسة.", - "stageLoading": "الاتصال بالمسرح...", - "stageEnded": "انتهى العرض التقديمي", - "roomEnded": "انتهى هذا الاجتماع", - "presenterLabel": "{{name}} يقدم", - "guestLink": "رابط الضيف", - "guestLinkOn": "رابط الضيوف مُفعّل. يمكن لأي شخص لديه الرابط مشاهدة العرض.", - "guestLinkOff": "رابط الضيف معطل.", - "copyLink": "نسخ الوصلة", - "rotateLink": "تدوير الرابط", - "disableLink": "تعطيل الرابط", - "rotateLinkConfirmTitle": "هل تريد تدوير رابط الضيف؟", - "rotateLinkDescription": "سيتوقف الرابط الحالي عن العمل فوراً. سيتم فصل الضيوف المتصلين عبر SSH.", - "disableLinkConfirmTitle": "هل تريد تعطيل رابط الضيف؟", - "disableLinkDescription": "سيتم قطع اتصال عارضي SSH المجهولين على الفور وسيتوقف الرابط الحالي عن العمل.", - "linkCopied": "تم نسخ الرابط", - "roles": "الأدوار", - "users": "المستخدمون", - "searchUsers": "ابحث عن المستخدمين", - "endConfirmTitle": "هل ننهي هذا الاجتماع؟", - "endDescription": "سينتهي {{name}} للجميع ولن يكون من الممكن إعادة فتحه.", - "endPersistentDescription": "سيتوقف العرض التقديمي النشط في {{name}} . وستبقى الغرفة متاحة لإعادة الاستخدام.", - "deleteRoom": "حذف الغرفة", - "deleteConfirmTitle": "هل تريد حذف هذه الغرفة؟", - "deleteDescription": "سيتم حذف {{name}} نهائياً للجميع ولا يمكن التراجع عنه.", - "invitedTo": "لقد تمت دعوتك إلى \"{{name}}\"", - "guest": { - "title": "مقابلة", - "waiting": "في انتظار المذيع...", - "linkInvalid": "رابط الضيف هذا غير صالح أو انتهى الاجتماع" - }, - "reopenFromPanel": "انتهت صلاحية علامة تبويب هذا الاجتماع. أعد فتح الغرفة من لوحة الاجتماعات.", - "requestControl": "التحكم في الطلبات", - "cancelControlRequest": "إلغاء الطلب", - "controlQueue": "طلبات التحكم", - "noControlRequests": "لا توجد طلبات معلقة.", - "grantControlTo": "قم بمنح التحكم إلى {{name}}", - "dismissControlRequest": "رفض طلب التحكم {{name}}", - "releaseControl": "التحكم في الإصدار", - "grantControl": "امنح السيطرة", - "revokeControl": "استعد السيطرة", - "controlRequestedBy": "طلب {{name}} التحكم", - "grant": "منحة", - "controlBadge": "مسيطر", - "youArePresenting": "أنت تعرض المحتوى في علامة تبويب أخرى. توقف عن العرض لتسليم المنصة.", - "created": "تم إنشاء الغرفة", - "invited": "الأعضاء المدعوون", - "roomTab": "مقابلة" - }, "sessionSharing": { - "guestView": { - "loading": "جارٍ الاتصال بالجلسة المشتركة...", - "linkInvalid": "رابط المشاركة هذا غير صالح أو منتهي الصلاحية أو تم إلغاؤه", - "rateLimited": "عدد المحاولات كبير جدًا، يرجى المحاولة مرة أخرى قريبًا", - "sessionEnded": "انتهت هذه الجلسة", - "readOnlyBadge": "للعرض فقط", - "ownerLabel": "مالك المضيف" - }, - "modalTitle": "جلسة مشاركة", - "shareButton": "يشارك", - "notReadyToShare": "الجلسة غير جاهزة للمشاركة بعد", - "modeTab": { - "link": "وصلة", - "user": "مستخدم" - }, - "linkModeDescription": "يمكن لأي شخص لديه هذا الرابط الانضمام، ولا يلزم وجود حساب.", - "userModeDescription": "شارك الملف مع مستخدم محدد لديه صلاحية الوصول إلى هذا المضيف. إذا لم يكن لديه صلاحية الوصول بعد، فشارك معه المضيف أولاً أو استخدم رابطًا. بمجرد المشاركة، ستظهر الجلسة في علامة تبويب \"الاتصالات\" الخاصة به.", "permissionLevel": { - "label": "مستوى الإذن", "readOnly": "للقراءة فقط", - "readOnlyDescription": "يمكنني مشاهدة الجلسة مباشرة ولكن لا يمكنني الكتابة أو التفاعل.", - "readWrite": "القراءة والكتابة", - "readWriteDescription": "يمكنه الكتابة والتفاعل مع الجلسة تمامًا مثل المالك." - }, - "expiryLabel": "انتهاء صلاحية الرابط", - "createLinkButton": "إنشاء رابط", - "createShareButton": "مشاركة مع المستخدم", - "searchUsersPlaceholder": "ابحث عن المستخدمين...", - "noUsersFound": "لم يتم العثور على مستخدمين", - "linkCreated": "تم إنشاء رابط المشاركة", - "linkCopied": "تم نسخ الرابط إلى الحافظة", - "copyLink": "نسخ الوصلة", - "shareCreated": "تمت مشاركة الجلسة. ستظهر في علامة تبويب \"الاتصالات\" الخاصة بهم.", - "shareFailed": "فشل إنشاء المشاركة", - "userLacksHostAccess": "لا يملك هذا المستخدم صلاحية الوصول إلى هذا المضيف بعد. شارك المضيف معه أولاً، أو استخدم رابطاً بدلاً من ذلك.", - "activeShares": "الأسهم النشطة", - "noActiveShares": "لا توجد أسهم نشطة لهذه الجلسة", - "revoke": "إلغاء", - "revokeConfirmTitle": "إلغاء هذه الحصة؟", - "revokeConfirmDescription": "أي شخص يستخدم هذا الملف المشترك سيفقد الوصول إليه فوراً.", - "revoked": "تم إلغاء الحصة", - "revokeFailed": "فشل في إلغاء المشاركة", - "joinCount": "{{count}} انضم", - "joinCount_other": "{{count}} ينضم", - "expiresAt": "تاريخ الانتهاء {{date}}", - "linkShareBadge": "وصلة", - "userShareBadge": "المستخدم: {{username}}", - "loadSharesFailed": "فشل تحميل المشاركات النشطة" + "readWrite": "القراءة والكتابة" + } }, "terminal": { "connect": "الاتصال بالمضيف", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "يرسل النظام إحصاءً يومياً مجهولاً للمستخدمين والمضيفين واستخدام الميزات للمساعدة في تحسين Termix. ولا يتم تضمين أي بيانات شخصية أو تفاصيل اتصال على الإطلاق.", "analyticsEnabledLockedDesc": "هذا الإعداد مقفل بواسطة متغير البيئة ENABLE_TELEMETRY ولا يمكن تغييره هنا.", "updateAnalyticsFailed": "فشل تحديث إعدادات التحليلات", - "sessionSharingGloballyEnabled": "السماح بمشاركة الجلسة", "auditForwardingTitle": "إعادة التوجيه إلى SIEM", "auditForwardingDesc": "أرسل نسخة من كل إدخال تدقيق إلى جامع بيانات خارجي بصيغة NDJSON. تبقى السجلات المحلية هي المصدر الموثوق.", "auditForwardingEnvNotice": "يُستخدم متغير البيئة أيضًا لضبط عملية إعادة التوجيه؛ ويأخذ عنوان URL المحفوظ هنا الأولوية.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "رمز حامل (مخزن)", "auditForwardingSaved": "تم حفظ إعدادات إعادة توجيه التدقيق", "auditForwardingSaveError": "فشل حفظ إعدادات إعادة توجيه التدقيق", - "sessionSharingGloballyEnabledDesc": "يسمح هذا الخيار بمشاركة جلسات الطرفية المباشرة، وجلسات RDP، وVNC، وTelnet على مستوى الجهاز. ويتجاوز هذا الخيار جميع إعدادات المشاركة الخاصة بكل مضيف عند تعطيله.", - "updateSessionSharingFailed": "فشل تحديث إعدادات مشاركة الجلسة", "sessionTimeout": "مهلة الجلسة", "hours": "ساعات", "sessionTimeoutRange": "الحد الأدنى 1 ساعة · الحد الأقصى 720 ساعة", diff --git a/src/ui/locales/translated/bg_BG.json b/src/ui/locales/translated/bg_BG.json index 6067a5a10..cddba0b38 100644 --- a/src/ui/locales/translated/bg_BG.json +++ b/src/ui/locales/translated/bg_BG.json @@ -667,7 +667,6 @@ "done_cli_link": "Отворете документацията на CLI" }, "nav": { - "collab": "Срещи", "home": "Дом", "terminal": "Терминал", "localTerminal": "Местен терминал", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Поддържайте вашата обвивка и всичко, което се изпълнява в нея, активно при прекъсвания на връзката и рестартиране на браузъра, като я стартирате вътре в tmux (tmux трябва да е инсталиран на хоста)", "enableSessionLogging": "Записване на сесии", "enableSessionLoggingDesc": "Записване на изхода от терминалната сесия за по-късен преглед", - "allowSessionSharing": "Разрешаване на споделяне на сесия", - "allowSessionSharingDesc": "Позволете споделянето на сесиите на живо на този хост чрез линк или с други потребители", "localEchoLabel": "Локално ехо", "localEchoDesc": "Предсказвайте безопасни за печат натискания на клавиши локално при бавни връзки. Подканите за парола и контролните въведени команди никога не се повтарят.", "localEchoDefault": "Използвайте глобална настройка", @@ -1571,137 +1568,11 @@ "join": "Присъединете се", "sharedSessionLabel": "{{hostName}} (споделено)" }, - "collab": { - "createRoom": "Нова стая", - "roomName": "Име на стаята", - "persistentRoom": "Постоянна стая", - "persistentRoomHint": "Постоянните стаи остават в списъка след края на срещата и могат да бъдат използвани повторно.", - "noRooms": "Все още няма стаи. Създайте една, за да започнете среща.", - "openRoom": "Отворено", - "endRoom": "Край на срещата", - "leaveRoom": "Оставете", - "deleteMember": "Премахване", - "removeMember": "Премахване на {{name}}", - "removeMemberTitle": "Да се премахне ли този член?", - "removeMemberDescription": "{{name}} ще загуби достъп до тази стая незабавно. Всеки активен SSH зрител ще бъде прекъснат.", - "invite": "Покани", - "inviteTitle": "Поканете членове", - "members": "Членове", - "membersPanel": "Членове на стаята и заявки за контрол", - "membersWithCount": "Членове ({{count}})", - "online": "онлайн", - "offline": "офлайн", - "you": "(ти)", - "hostBadge": "Домакин", - "presenterBadge": "Представяне", - "present": "Настояще", - "presentTitle": "Излезте на сцената", - "presentHost": "Домакин", - "searchHosts": "Търсене на хостове", - "noHostsFound": "Няма намерени налични хостове.", - "presentProtocol": "Протокол", - "stopPresenting": "Спиране на представянето", - "takeOver": "Поемете контрола", - "takeOverConfirmTitle": "Да се замени ли текущата презентация?", - "takeOverDescription": "{{name}} представя сега. Поемането на водещия ще изключи всички от тази сцена.", - "emptyStage": "Никой не представя. Излезте на сцената, за да споделите сесия.", - "stageLoading": "Свързване със сцената...", - "stageEnded": "Презентацията приключи", - "roomEnded": "Тази среща приключи", - "presenterLabel": "{{name}} представя", - "guestLink": "Връзка за гости", - "guestLinkOn": "Връзката за гости е включена. Всеки с връзката може да гледа сцената.", - "guestLinkOff": "Връзката за гости е изключена.", - "copyLink": "Копиране на връзката", - "rotateLink": "Завъртане на връзката", - "disableLink": "Деактивиране на връзката", - "rotateLinkConfirmTitle": "Да се завърти ли линкът за гости?", - "rotateLinkDescription": "Текущата връзка ще спре да работи незабавно. Свързаните SSH гости ще бъдат прекъснати.", - "disableLinkConfirmTitle": "Да се деактивира ли връзката за гости?", - "disableLinkDescription": "Анонимните SSH зрители ще бъдат незабавно прекъснати и текущата връзка ще спре да работи.", - "linkCopied": "Връзката е копирана", - "roles": "Роли", - "users": "Потребители", - "searchUsers": "Търсене на потребители", - "endConfirmTitle": "Да се прекрати ли тази среща?", - "endDescription": "{{name}} ще приключи за всички и не може да бъде отворено отново.", - "endPersistentDescription": "Активната презентация в {{name}} ще спре. Стаята остава достъпна за повторна употреба.", - "deleteRoom": "Изтриване на стая", - "deleteConfirmTitle": "Да се изтрие ли тази стая?", - "deleteDescription": "{{name}} ще бъде изтрито за постоянно за всички и действието не може да бъде отменено.", - "invitedTo": "Поканиха ви в „{{name}}“", - "guest": { - "title": "Среща", - "waiting": "В очакване на водещия...", - "linkInvalid": "Този линк за гости е невалиден или срещата е приключила" - }, - "reopenFromPanel": "Този раздел за среща е с изтекъл срок. Отворете отново стаята от панела „Срещи“.", - "requestControl": "Заявка за контрол", - "cancelControlRequest": "Отмяна на заявката", - "controlQueue": "Заявки за контрол", - "noControlRequests": "Няма чакащи заявки.", - "grantControlTo": "Дайте контрол на {{name}}", - "dismissControlRequest": "Отхвърляне на заявката за контрол на {{name}}", - "releaseControl": "Контрол на освобождаването", - "grantControl": "Дайте контрол", - "revokeControl": "Върнете си контрола", - "controlRequestedBy": "{{name}} поиска контрол", - "grant": "Грант", - "controlBadge": "В контрол", - "youArePresenting": "Презентирате в друг раздел. Спрете презентацията, за да предадете сцената.", - "created": "Стаята е създадена", - "invited": "Поканени членове", - "roomTab": "Среща" - }, "sessionSharing": { - "guestView": { - "loading": "Свързване със споделена сесия...", - "linkInvalid": "Този линк за споделяне е невалиден, изтекъл или е отменен", - "rateLimited": "Твърде много опити, моля опитайте отново скоро", - "sessionEnded": "Тази сесия приключи", - "readOnlyBadge": "Само преглед", - "ownerLabel": "Собственик на домакин" - }, - "modalTitle": "Споделяне на сесия", - "shareButton": "Споделяне", - "notReadyToShare": "Сесията все още не е готова за споделяне", - "modeTab": { - "link": "Връзка", - "user": "Потребител" - }, - "linkModeDescription": "Всеки с този линк може да се присъедини, не е необходим акаунт.", - "userModeDescription": "Споделете с конкретен потребител, който вече има достъп до този хост. Ако все още няма достъп, първо споделете хоста с него или използвайте връзка. След като бъде споделена, сесията се показва в раздела му „Връзки“.", "permissionLevel": { - "label": "Ниво на разрешение", "readOnly": "Само за четене", - "readOnlyDescription": "Може да гледа сесията на живо, но не може да пише или да взаимодейства.", - "readWrite": "Четене и запис", - "readWriteDescription": "Може да пише и да взаимодейства със сесията точно като собственика." - }, - "expiryLabel": "Изтичане на срока на валидност на връзката", - "createLinkButton": "Създаване на връзка", - "createShareButton": "Споделяне с потребителя", - "searchUsersPlaceholder": "Търсене на потребители...", - "noUsersFound": "Не са намерени потребители", - "linkCreated": "Линкът за споделяне е създаден", - "linkCopied": "Връзката е копирана в буферната памет", - "copyLink": "Копиране на връзката", - "shareCreated": "Сесията е споделена. Тя ще се покаже в раздела им „Връзки“.", - "shareFailed": "Създаването на споделено съдържание не бе успешно", - "userLacksHostAccess": "Тозият потребител все още няма достъп до този хост. Първо споделете хоста с него или използвайте линк.", - "activeShares": "Активни акции", - "noActiveShares": "Няма активни споделяния за тази сесия", - "revoke": "Отмяна", - "revokeConfirmTitle": "Да се отмени ли това споделяне?", - "revokeConfirmDescription": "Всеки, който използва това споделяне, ще загуби достъп незабавно.", - "revoked": "Акцията е отменена", - "revokeFailed": "Неуспешно отменяне на споделянето", - "joinCount": "{{count}} присъединяване", - "joinCount_other": "{{count}} се присъединява", - "expiresAt": "Изтича {{date}}", - "linkShareBadge": "Връзка", - "userShareBadge": "Потребител: {{username}}", - "loadSharesFailed": "Зареждането на активните споделяния не бе успешно" + "readWrite": "Четене и запис" + } }, "terminal": { "connect": "Свързване с хоста", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Изпраща анонимен дневен брой потребители, хостове и използване на функции, за да помогне за подобряването на Termix. Никога не се включват лични данни или подробности за връзката.", "analyticsEnabledLockedDesc": "Тази настройка е заключена от променливата на средата ENABLE_TELEMETRY и не може да бъде променена тук.", "updateAnalyticsFailed": "Актуализирането на настройката за анализ не бе успешно", - "sessionSharingGloballyEnabled": "Разрешаване на споделяне на сесия", "auditForwardingTitle": "Напред към SIEM", "auditForwardingDesc": "Изпращайте копие на всеки запис от одит до външен колектор като NDJSON. Локалните регистрационни файлове остават източник на достоверни данни.", "auditForwardingEnvNotice": "Променлива на средата също конфигурира пренасочването; URL адресът, записан тук, има приоритет.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Токен на приносителя (съхранен)", "auditForwardingSaved": "Настройките за пренасочване на одита са запазени", "auditForwardingSaveError": "Запазването на настройките за пренасочване на одит не бе успешно", - "sessionSharingGloballyEnabledDesc": "Разрешава споделянето на сесии на живо на терминал, RDP, VNC и Telnet в целия екземпляр. Заменя всяко превключвател за споделяне на хост, когато е деактивиран.", - "updateSessionSharingFailed": "Актуализирането на настройката за споделяне на сесията не бе успешно", "sessionTimeout": "Време за изчакване на сесията", "hours": "часове", "sessionTimeoutRange": "Мин. 1 ч. · Макс. 720 ч.", diff --git a/src/ui/locales/translated/bn_BD.json b/src/ui/locales/translated/bn_BD.json index a5a78f235..076564d46 100644 --- a/src/ui/locales/translated/bn_BD.json +++ b/src/ui/locales/translated/bn_BD.json @@ -667,7 +667,6 @@ "done_cli_link": "CLI ডক্স খুলুন" }, "nav": { - "collab": "সভা", "home": "হোম", "terminal": "টার্মিনাল", "localTerminal": "স্থানীয় টার্মিনাল", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "সংযোগ বিচ্ছিন্ন হওয়া এবং ব্রাউজার পুনরায় চালু হওয়ার পরেও আপনার শেল এবং এর মধ্যে চলমান সবকিছু সচল রাখতে tmux-এর ভিতরে এটি চালান (tmux অবশ্যই হোস্টে ইনস্টল করা থাকতে হবে)।", "enableSessionLogging": "সেশন লগিং", "enableSessionLoggingDesc": "পরবর্তী পর্যালোচনার জন্য টার্মিনাল সেশনের আউটপুট রেকর্ড করুন।", - "allowSessionSharing": "সেশন শেয়ারিং অনুমোদন করুন", - "allowSessionSharingDesc": "এই হোস্টের লাইভ সেশনগুলো লিঙ্কের মাধ্যমে বা অন্যান্য ব্যবহারকারীদের সাথে শেয়ার করা যাবে।", "localEchoLabel": "স্থানীয় প্রতিধ্বনি", "localEchoDesc": "ধীরগতির সংযোগে স্থানীয়ভাবে নিরাপদ ও মুদ্রণযোগ্য কীস্ট্রোক অনুমান করুন। পাসওয়ার্ড প্রম্পট এবং কন্ট্রোল ইনপুট কখনও প্রতিধ্বনিত হয় না।", "localEchoDefault": "গ্লোবাল সেটিং ব্যবহার করুন", @@ -1571,137 +1568,11 @@ "join": "যোগদান করুন", "sharedSessionLabel": "{{hostName}} (ভাগ করা)" }, - "collab": { - "createRoom": "নতুন ঘর", - "roomName": "রুমের নাম", - "persistentRoom": "স্থায়ী কক্ষ", - "persistentRoomHint": "মিটিং শেষ হওয়ার পরেও স্থায়ী রুমগুলো তালিকাভুক্ত থাকে এবং পুনরায় ব্যবহার করা যায়।", - "noRooms": "এখনো কোনো রুম নেই। মিটিং শুরু করতে একটি রুম তৈরি করুন।", - "openRoom": "খোলা", - "endRoom": "সভা শেষ করুন", - "leaveRoom": "ছেড়ে দিন", - "deleteMember": "অপসারণ করুন", - "removeMember": "{{name}} মুছে ফেলুন", - "removeMemberTitle": "এই সদস্যকে অপসারণ করবেন?", - "removeMemberDescription": "{{name}} অবিলম্বে এই রুমে প্রবেশাধিকার হারাবে। যেকোনো সক্রিয় SSH ভিউয়ার সংযোগ বিচ্ছিন্ন হয়ে যাবে।", - "invite": "আমন্ত্রণ", - "inviteTitle": "সদস্যদের আমন্ত্রণ জানান", - "members": "সদস্যরা", - "membersPanel": "রুমের সদস্য এবং নিয়ন্ত্রণ অনুরোধ", - "membersWithCount": "সদস্যগণ ({{count}})", - "online": "অনলাইন", - "offline": "অফলাইন", - "you": "(তুমি)", - "hostBadge": "হোস্ট", - "presenterBadge": "উপস্থাপন করা হচ্ছে", - "present": "বর্তমান", - "presentTitle": "মঞ্চে উঠুন", - "presentHost": "হোস্ট", - "searchHosts": "হোস্ট অনুসন্ধান করুন", - "noHostsFound": "কোনো উপলব্ধ হোস্ট খুঁজে পাওয়া যায়নি।", - "presentProtocol": "প্রোটোকল", - "stopPresenting": "উপস্থাপন করা বন্ধ করুন", - "takeOver": "দায়িত্ব গ্রহণ করুন", - "takeOverConfirmTitle": "বর্তমান উপস্থাপনাটি প্রতিস্থাপন করবেন?", - "takeOverDescription": "{{name}} এখন উপস্থাপনা করছে। দায়িত্ব গ্রহণ করলে সবাই সেই মঞ্চ থেকে বিচ্ছিন্ন হয়ে যাবে।", - "emptyStage": "কেউ উপস্থাপনা করছেন না। একটি সেশন পরিচালনা করতে মঞ্চে আসুন।", - "stageLoading": "মঞ্চের সাথে সংযোগ স্থাপন...", - "stageEnded": "উপস্থাপনা শেষ হলো।", - "roomEnded": "এই সভা শেষ হয়েছে।", - "presenterLabel": "{{name}} উপস্থাপন করছে", - "guestLink": "অতিথি লিঙ্ক", - "guestLinkOn": "গেস্ট লিঙ্ক চালু আছে। লিঙ্কটি ব্যবহার করে যে কেউ মঞ্চটি দেখতে পারবেন।", - "guestLinkOff": "গেস্ট লিঙ্ক বন্ধ আছে।", - "copyLink": "লিঙ্ক কপি করুন", - "rotateLink": "লিঙ্ক ঘোরান", - "disableLink": "লিঙ্ক নিষ্ক্রিয় করুন", - "rotateLinkConfirmTitle": "গেস্ট লিঙ্কটি ঘোরান?", - "rotateLinkDescription": "বর্তমান সংযোগটি অবিলম্বে কাজ করা বন্ধ করে দেবে। সংযুক্ত SSH গেস্টদের সংযোগ বিচ্ছিন্ন করা হবে।", - "disableLinkConfirmTitle": "গেস্ট লিঙ্কটি নিষ্ক্রিয় করবেন?", - "disableLinkDescription": "বেনামী SSH দর্শকদের সংযোগ অবিলম্বে বিচ্ছিন্ন করা হবে এবং বর্তমান লিঙ্কটি কাজ করা বন্ধ করে দেবে।", - "linkCopied": "লিঙ্ক কপি করা হয়েছে", - "roles": "ভূমিকা", - "users": "ব্যবহারকারীরা", - "searchUsers": "ব্যবহারকারীদের অনুসন্ধান করুন", - "endConfirmTitle": "এই সভা শেষ করব?", - "endDescription": "{{name}} সকলের জন্য শেষ হয়ে যাবে এবং পুনরায় খোলা যাবে না।", - "endPersistentDescription": "{{name}} -এ চলমান উপস্থাপনাটি বন্ধ হয়ে যাবে। রুমটি পুনরায় ব্যবহারের জন্য উপলব্ধ থাকবে।", - "deleteRoom": "রুম মুছুন", - "deleteConfirmTitle": "এই রুমটা মুছে ফেলবেন?", - "deleteDescription": "{{name}} সকলের জন্য স্থায়ীভাবে মুছে ফেলা হবে এবং এটি আর পূর্বাবস্থায় ফেরানো যাবে না।", - "invitedTo": "তোমাকে \"{{name}} \" এ আমন্ত্রণ জানানো হয়েছিল", - "guest": { - "title": "সভা", - "waiting": "উপস্থাপকের জন্য অপেক্ষা করছি...", - "linkInvalid": "এই অতিথি লিঙ্কটি অবৈধ অথবা মিটিং শেষ হয়ে গেছে।" - }, - "reopenFromPanel": "এই মিটিং ট্যাবটির মেয়াদ শেষ হয়ে গেছে। মিটিং প্যানেল থেকে রুমটি পুনরায় খুলুন।", - "requestControl": "অনুরোধ নিয়ন্ত্রণ", - "cancelControlRequest": "অনুরোধ বাতিল করুন", - "controlQueue": "নিয়ন্ত্রণ অনুরোধ", - "noControlRequests": "কোনো অপেক্ষমান অনুরোধ নেই।", - "grantControlTo": "{{name}}-কে নিয়ন্ত্রণ দিন", - "dismissControlRequest": "{{name}}এর নিয়ন্ত্রণ অনুরোধ খারিজ করুন", - "releaseControl": "রিলিজ নিয়ন্ত্রণ", - "grantControl": "নিয়ন্ত্রণ দিন", - "revokeControl": "নিয়ন্ত্রণ ফিরিয়ে নিন", - "controlRequestedBy": "{{name}} নিয়ন্ত্রণের জন্য অনুরোধ করেছে", - "grant": "অনুদান", - "controlBadge": "নিয়ন্ত্রণে", - "youArePresenting": "আপনি অন্য একটি ট্যাবে প্রেজেন্টেশন দিচ্ছেন। মঞ্চ হস্তান্তর করার জন্য প্রেজেন্টেশন বন্ধ করুন।", - "created": "রুম তৈরি করা হয়েছে", - "invited": "সদস্যদের আমন্ত্রণ জানানো হয়েছে", - "roomTab": "সভা" - }, "sessionSharing": { - "guestView": { - "loading": "শেয়ার করা সেশনে সংযোগ স্থাপন করা হচ্ছে...", - "linkInvalid": "এই শেয়ার লিঙ্কটি অবৈধ, মেয়াদোত্তীর্ণ, বা বাতিল করা হয়েছে।", - "rateLimited": "অনেকবার চেষ্টা করা হয়েছে, অনুগ্রহ করে কিছুক্ষণ পরে আবার চেষ্টা করুন।", - "sessionEnded": "এই অধিবেশন শেষ হয়েছে", - "readOnlyBadge": "শুধুমাত্র দেখার জন্য", - "ownerLabel": "হোস্ট মালিক" - }, - "modalTitle": "সেশন শেয়ার করুন", - "shareButton": "শেয়ার", - "notReadyToShare": "সেশনটি এখনও শেয়ার করার জন্য প্রস্তুত নয়।", - "modeTab": { - "link": "লিঙ্ক", - "user": "ব্যবহারকারী" - }, - "linkModeDescription": "এই লিঙ্কের মাধ্যমে যে কেউ যোগ দিতে পারবেন, কোনো অ্যাকাউন্টের প্রয়োজন নেই।", - "userModeDescription": "এমন একজন নির্দিষ্ট ব্যবহারকারীর সাথে শেয়ার করুন যার এই হোস্টে আগে থেকেই অ্যাক্সেস আছে। যদি তাদের এখনও অ্যাক্সেস না থাকে, তাহলে প্রথমে তাদের সাথে হোস্টটি শেয়ার করুন অথবা এর পরিবর্তে একটি লিঙ্ক ব্যবহার করুন। শেয়ার করা হয়ে গেলে, সেশনটি তাদের 'কানেকশনস' ট্যাবে দেখা যাবে।", "permissionLevel": { - "label": "অনুমতি স্তর", "readOnly": "শুধুমাত্র পঠনযোগ্য", - "readOnlyDescription": "সেশনটি সরাসরি দেখা যাবে কিন্তু টাইপ বা অংশগ্রহণ করা যাবে না।", - "readWrite": "পড়া-লেখা", - "readWriteDescription": "মালিকের মতোই সেশনে টাইপ করতে এবং ইন্টারঅ্যাক্ট করতে পারবেন।" - }, - "expiryLabel": "লিঙ্কের মেয়াদ শেষ", - "createLinkButton": "লিঙ্ক তৈরি করুন", - "createShareButton": "ব্যবহারকারীর সাথে শেয়ার করুন", - "searchUsersPlaceholder": "ব্যবহারকারীদের অনুসন্ধান করুন...", - "noUsersFound": "কোন ব্যবহারকারী খুঁজে পাওয়া যায়নি", - "linkCreated": "শেয়ার লিঙ্ক তৈরি করা হয়েছে", - "linkCopied": "লিঙ্ক ক্লিপবোর্ডে কপি করা হয়েছে", - "copyLink": "লিঙ্ক কপি করুন", - "shareCreated": "সেশনটি শেয়ার করা হয়েছে। এটি তাদের কানেকশনস ট্যাবে দেখা যাবে।", - "shareFailed": "শেয়ার তৈরি করতে ব্যর্থ হয়েছে", - "userLacksHostAccess": "ওই ব্যবহারকারীর এখনও এই হোস্টে অ্যাক্সেস নেই। প্রথমে তাদের সাথে হোস্টটি শেয়ার করুন, অথবা এর পরিবর্তে একটি লিঙ্ক ব্যবহার করুন।", - "activeShares": "সক্রিয় শেয়ার", - "noActiveShares": "এই সেশনের জন্য কোনো সক্রিয় শেয়ার নেই", - "revoke": "বাতিল করুন", - "revokeConfirmTitle": "এই শেয়ারটি প্রত্যাহার করবেন?", - "revokeConfirmDescription": "যে কেউ এই শেয়ারটি ব্যবহার করলে, তিনি অবিলম্বে অ্যাক্সেস হারাবেন।", - "revoked": "শেয়ার প্রত্যাহার করা হয়েছে", - "revokeFailed": "শেয়ার প্রত্যাহার করতে ব্যর্থ হয়েছে", - "joinCount": "{{count}} যোগদান করুন", - "joinCount_other": "{{count}} যোগদান করে", - "expiresAt": "মেয়াদ শেষ হবে {{date}}", - "linkShareBadge": "লিঙ্ক", - "userShareBadge": "ব্যবহারকারী: {{username}}", - "loadSharesFailed": "সক্রিয় শেয়ারগুলি লোড করতে ব্যর্থ হয়েছে" + "readWrite": "পড়া-লেখা" + } }, "terminal": { "connect": "হোস্টের সাথে সংযোগ করুন", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "টারমিক্সকে উন্নত করতে সাহায্য করার জন্য ব্যবহারকারী, হোস্ট এবং ফিচার ব্যবহারের একটি বেনামী দৈনিক গণনা পাঠায়। এতে কোনো ব্যক্তিগত তথ্য বা সংযোগের বিবরণ অন্তর্ভুক্ত করা হয় না।", "analyticsEnabledLockedDesc": "এই সেটিংটি ENABLE_TELEMETRY এনভায়রনমেন্ট ভেরিয়েবল দ্বারা লক করা আছে এবং এখান থেকে এটি পরিবর্তন করা যাবে না।", "updateAnalyticsFailed": "অ্যানালিটিক্স সেটিং আপডেট করতে ব্যর্থ হয়েছে", - "sessionSharingGloballyEnabled": "সেশন শেয়ারিং অনুমোদন করুন", "auditForwardingTitle": "SIEM-এ ফরোয়ার্ড করুন", "auditForwardingDesc": "প্রতিটি অডিট এন্ট্রির একটি কপি NDJSON ফরম্যাটে একজন বহিরাগত সংগ্রাহকের কাছে পাঠান। স্থানীয় লগই তথ্যের নির্ভরযোগ্য উৎস হিসেবে থাকবে।", "auditForwardingEnvNotice": "একটি এনভায়রনমেন্ট ভেরিয়েবল ফরওয়ার্ডিংও কনফিগার করে; এখানে সংরক্ষিত URL-টি অগ্রাধিকার পায়।", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "বাহক টোকেন (সংরক্ষিত)", "auditForwardingSaved": "অডিট ফরওয়ার্ডিং সেটিংস সংরক্ষণ করা হয়েছে", "auditForwardingSaveError": "অডিট ফরওয়ার্ডিং সেটিংস সংরক্ষণ করতে ব্যর্থ হয়েছে", - "sessionSharingGloballyEnabledDesc": "লাইভ টার্মিনাল, RDP, VNC, এবং টেলনেট সেশনগুলোকে ইনস্ট্যান্স-ব্যাপী শেয়ার করার অনুমতি দিন। এটি নিষ্ক্রিয় করা হলে, প্রতি-হোস্ট শেয়ারিং টগলকে ওভাররাইড করে।", - "updateSessionSharingFailed": "সেশন শেয়ারিং সেটিং আপডেট করতে ব্যর্থ হয়েছে", "sessionTimeout": "সেশন টাইমআউট", "hours": "ঘন্টা", "sessionTimeoutRange": "সর্বনিম্ন ১ ঘণ্টা · সর্বোচ্চ ৭২০ ঘণ্টা", diff --git a/src/ui/locales/translated/ca_ES.json b/src/ui/locales/translated/ca_ES.json index 0396fea9f..0f7e811c2 100644 --- a/src/ui/locales/translated/ca_ES.json +++ b/src/ui/locales/translated/ca_ES.json @@ -667,7 +667,6 @@ "done_cli_link": "Obriu la documentació de la CLI" }, "nav": { - "collab": "Reunions", "home": "Inici", "terminal": "Terminal", "localTerminal": "Terminal local", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Mantingueu actiu el vostre shell i tot el que s'hi executi després de desconnexions i reinicis del navegador executant-lo dins de tmux (tmux ha d'estar instal·lat a l'amfitrió)", "enableSessionLogging": "Registre de sessions", "enableSessionLoggingDesc": "Enregistra la sortida de la sessió de terminal per a una revisió posterior", - "allowSessionSharing": "Permetre la compartició de sessions", - "allowSessionSharingDesc": "Permet que les sessions en directe d'aquest amfitrió es comparteixin mitjançant un enllaç o amb altres usuaris", "localEchoLabel": "Eco local", "localEchoDesc": "Prediu localment les pulsacions de tecles imprimibles de manera segura en connexions lentes. Les sol·licituds de contrasenya i l'entrada de control no es reprodueixen mai.", "localEchoDefault": "Utilitza la configuració global", @@ -1571,137 +1568,11 @@ "join": "Uneix-te", "sharedSessionLabel": "{{hostName}} (compartit)" }, - "collab": { - "createRoom": "Nova habitació", - "roomName": "Nom de l'habitació", - "persistentRoom": "Sala persistent", - "persistentRoomHint": "Les sales persistents romanen a la llista després que finalitzi la reunió i es poden reutilitzar.", - "noRooms": "Encara no hi ha cap sala. Crea'n una per iniciar una reunió.", - "openRoom": "Obre", - "endRoom": "Finalitza la reunió", - "leaveRoom": "Marxar", - "deleteMember": "Elimina", - "removeMember": "Elimina {{name}}", - "removeMemberTitle": "Voleu eliminar aquest membre?", - "removeMemberDescription": "{{name}} perdrà l'accés a aquesta sala immediatament. Qualsevol visor SSH actiu es desconnectarà.", - "invite": "Convidar", - "inviteTitle": "Convida membres", - "members": "Membres", - "membersPanel": "Membres de la sala i sol·licituds de control", - "membersWithCount": "Membres ({{count}})", - "online": "en línia", - "offline": "fora de línia", - "you": "(tu)", - "hostBadge": "Amfitrió", - "presenterBadge": "Presentant", - "present": "Present", - "presentTitle": "Puja a l'escenari", - "presentHost": "Amfitrió", - "searchHosts": "Cerca amfitrions", - "noHostsFound": "No s'han trobat amfitrions disponibles.", - "presentProtocol": "Protocol", - "stopPresenting": "Atura la presentació", - "takeOver": "Prendre el control", - "takeOverConfirmTitle": "Voleu substituir la presentació actual?", - "takeOverDescription": "{{name}} està presentant ara. Prendre el control desconnectarà tothom d'aquesta etapa.", - "emptyStage": "Ningú fa una presentació. Puja a l'escenari per compartir una sessió.", - "stageLoading": "Connexió amb l'escenari...", - "stageEnded": "La presentació va acabar", - "roomEnded": "Aquesta reunió ha acabat", - "presenterLabel": "{{name}} presenta", - "guestLink": "Enllaç de convidat", - "guestLinkOn": "L'enllaç de convidat està activat. Qualsevol persona que tingui l'enllaç pot veure l'escenari.", - "guestLinkOff": "L'enllaç de convidat està desactivat.", - "copyLink": "Copia l'enllaç", - "rotateLink": "Gira l'enllaç", - "disableLink": "Desactiva l'enllaç", - "rotateLinkConfirmTitle": "Voleu girar l'enllaç de convidat?", - "rotateLinkDescription": "L'enllaç actual deixarà de funcionar immediatament. Els convidats SSH connectats es desconnectaran.", - "disableLinkConfirmTitle": "Voleu desactivar l'enllaç de convidat?", - "disableLinkDescription": "Els visors SSH anònims es desconnectaran immediatament i l'enllaç actual deixarà de funcionar.", - "linkCopied": "Enllaç copiat", - "roles": "Rols", - "users": "Usuaris", - "searchUsers": "Cerca usuaris", - "endConfirmTitle": "Voleu finalitzar aquesta reunió?", - "endDescription": "{{name}} finalitzarà per a tothom i no es podrà tornar a obrir.", - "endPersistentDescription": "La presentació activa a {{name}} s'aturarà. La sala romandrà disponible per a reutilització.", - "deleteRoom": "Suprimeix l'habitació", - "deleteConfirmTitle": "Voleu suprimir aquesta sala?", - "deleteDescription": "{{name}} s'eliminarà permanentment per a tothom i no es podrà desfer.", - "invitedTo": "Has estat convidat a \"{{name}}\"", - "guest": { - "title": "Reunió", - "waiting": "Esperant el presentador...", - "linkInvalid": "Aquest enllaç de convidat no és vàlid o la reunió ha finalitzat." - }, - "reopenFromPanel": "Aquesta pestanya de reunió ha caducat. Torna a obrir la sala des del tauler Reunions.", - "requestControl": "Sol·licitud de control", - "cancelControlRequest": "Cancel·la la sol·licitud", - "controlQueue": "Sol·licituds de control", - "noControlRequests": "No hi ha sol·licituds pendents.", - "grantControlTo": "Dóna el control a {{name}}", - "dismissControlRequest": "Ignora la sol·licitud de control de {{name}}", - "releaseControl": "Control de llançament", - "grantControl": "Donar control", - "revokeControl": "Recupera el control", - "controlRequestedBy": "{{name}} va demanar control", - "grant": "Subvenció", - "controlBadge": "En control", - "youArePresenting": "Esteu presentant en una altra pestanya. Deixeu de presentar per cedir l'escenari.", - "created": "Sala creada", - "invited": "Membres convidats", - "roomTab": "Reunió" - }, "sessionSharing": { - "guestView": { - "loading": "Connectant a la sessió compartida...", - "linkInvalid": "Aquest enllaç per compartir no és vàlid, ha caducat o s'ha revocat.", - "rateLimited": "Massa intents, si us plau, torneu-ho a intentar en breu", - "sessionEnded": "Aquesta sessió ha finalitzat", - "readOnlyBadge": "Només visualització", - "ownerLabel": "Propietari de l'amfitrió" - }, - "modalTitle": "Comparteix la sessió", - "shareButton": "Comparteix", - "notReadyToShare": "La sessió encara no està a punt per compartir", - "modeTab": { - "link": "Enllaç", - "user": "Usuari" - }, - "linkModeDescription": "Qualsevol persona amb aquest enllaç s'hi pot unir, no cal tenir cap compte.", - "userModeDescription": "Comparteix amb un usuari específic que ja tingui accés a aquest amfitrió. Si encara no hi té accés, comparteix primer l'amfitrió amb ell o utilitza un enllaç. Un cop compartida, la sessió apareix a la pestanya Connexions.", "permissionLevel": { - "label": "Nivell de permís", "readOnly": "Només lectura", - "readOnlyDescription": "Puc veure la sessió en directe però no puc escriure ni interactuar.", - "readWrite": "Lectura i escriptura", - "readWriteDescription": "Pot escriure i interactuar amb la sessió igual que el propietari." - }, - "expiryLabel": "Caducitat de l'enllaç", - "createLinkButton": "Crea un enllaç", - "createShareButton": "Comparteix amb l'usuari", - "searchUsersPlaceholder": "Cerca usuaris...", - "noUsersFound": "No s'han trobat usuaris", - "linkCreated": "Enllaç de compartició creat", - "linkCopied": "Enllaç copiat al porta-retalls", - "copyLink": "Copia l'enllaç", - "shareCreated": "Sessió compartida. Apareixerà a la pestanya Connexions.", - "shareFailed": "No s'ha pogut crear la compartició", - "userLacksHostAccess": "Aquest usuari encara no té accés a aquest amfitrió. Primer comparteix l'amfitrió amb ell o utilitza un enllaç.", - "activeShares": "Accions actives", - "noActiveShares": "No hi ha cap compartició activa per a aquesta sessió", - "revoke": "Revocar", - "revokeConfirmTitle": "Voleu revocar aquesta compartició?", - "revokeConfirmDescription": "Qualsevol persona que utilitzi aquesta opció compartida perdrà l'accés immediatament.", - "revoked": "Compartir revocat", - "revokeFailed": "No s'ha pogut revocar la compartició", - "joinCount": "{{count}} uneix-te", - "joinCount_other": "{{count}} s'uneix", - "expiresAt": "Caduca {{date}}", - "linkShareBadge": "Enllaç", - "userShareBadge": "Usuari: {{username}}", - "loadSharesFailed": "No s'han pogut carregar les comparticions actives" + "readWrite": "Lectura i escriptura" + } }, "terminal": { "connect": "Connecta't a l'amfitrió", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Envia un recompte diari anònim d'usuaris, amfitrions i ús de funcions per ajudar a millorar Termix. No s'hi inclouen mai dades personals ni detalls de connexió.", "analyticsEnabledLockedDesc": "Aquesta configuració està bloquejada per la variable d'entorn ENABLE_TELEMETRY i no es pot canviar aquí.", "updateAnalyticsFailed": "No s'ha pogut actualitzar la configuració d'analítica", - "sessionSharingGloballyEnabled": "Permetre la compartició de sessions", "auditForwardingTitle": "Envia a SIEM", "auditForwardingDesc": "Envieu una còpia de cada entrada d'auditoria a un col·lector extern com a NDJSON. Els registres locals continuen sent la font de veritat.", "auditForwardingEnvNotice": "Una variable d'entorn també configura el reenviament; l'URL desada aquí té prioritat.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "testimoni del portador (emmagatzemat)", "auditForwardingSaved": "Configuració de reenviament d'auditoria desada", "auditForwardingSaveError": "No s'ha pogut desar la configuració de reenviament d'auditoria", - "sessionSharingGloballyEnabledDesc": "Permet que les sessions de terminal, RDP, VNC i Telnet en directe es comparteixin a tota la instància. Anul·la tots els activables i desactivables per compartir per host quan estan desactivats.", - "updateSessionSharingFailed": "No s'ha pogut actualitzar la configuració de compartició de sessions", "sessionTimeout": "Temps d'espera de la sessió", "hours": "hores", "sessionTimeoutRange": "Mín 1 h · Màx 720 h", diff --git a/src/ui/locales/translated/cs_CZ.json b/src/ui/locales/translated/cs_CZ.json index e94c4efcc..a0ce7d92a 100644 --- a/src/ui/locales/translated/cs_CZ.json +++ b/src/ui/locales/translated/cs_CZ.json @@ -667,7 +667,6 @@ "done_cli_link": "Otevřete dokumentaci k rozhraní příkazového řádku" }, "nav": { - "collab": "Schůzky", "home": "Domov", "terminal": "Terminál", "localTerminal": "Místní terminál", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Udržujte svůj shell a vše, co v něm běží, aktivní i po odpojení a restartu prohlížeče spuštěním uvnitř tmuxu (tmux musí být nainstalován na hostiteli).", "enableSessionLogging": "Protokolování relací", "enableSessionLoggingDesc": "Zaznamenejte výstup terminálové relace pro pozdější kontrolu", - "allowSessionSharing": "Povolit sdílení relace", - "allowSessionSharingDesc": "Umožnit sdílení živých relací na tomto hostiteli prostřednictvím odkazu nebo s ostatními uživateli", "localEchoLabel": "Místní ozvěna", "localEchoDesc": "Předvídat bezpečně tisknutelné stisky kláves lokálně na pomalém připojení. Výzvy k zadání hesla a ovládací prvky se nikdy nezobrazují.", "localEchoDefault": "Použít globální nastavení", @@ -1571,137 +1568,11 @@ "join": "Připojit se", "sharedSessionLabel": "{{hostName}} (sdílené)" }, - "collab": { - "createRoom": "Nový pokoj", - "roomName": "Název místnosti", - "persistentRoom": "Trvalá místnost", - "persistentRoomHint": "Trvalé místnosti zůstanou uvedeny i po skončení schůzky a lze je znovu použít.", - "noRooms": "Zatím žádné místnosti. Vytvořte si novou a začněte schůzku.", - "openRoom": "OTEVŘENO", - "endRoom": "Ukončit schůzku", - "leaveRoom": "Dovolená", - "deleteMember": "Odstranit", - "removeMember": "Odebrat {{name}}", - "removeMemberTitle": "Odebrat tohoto člena?", - "removeMemberDescription": "{{name}} okamžitě ztratí přístup k této místnosti. Jakýkoli aktivní SSH prohlížeč bude odpojen.", - "invite": "Pozvat", - "inviteTitle": "Pozvat členy", - "members": "Členové", - "membersPanel": "Členové místnosti a požadavky na ovládání", - "membersWithCount": "Členové ({{count}})", - "online": "online", - "offline": "offline", - "you": "(Vy)", - "hostBadge": "Hostitel", - "presenterBadge": "Prezentace", - "present": "Současnost", - "presentTitle": "Vyjdi na pódium", - "presentHost": "Hostitel", - "searchHosts": "Vyhledávání hostitelů", - "noHostsFound": "Nebyli nalezeni žádní dostupní hostitelé.", - "presentProtocol": "Protokol", - "stopPresenting": "Zastavit prezentaci", - "takeOver": "Převzít", - "takeOverConfirmTitle": "Nahradit aktuální prezentaci?", - "takeOverDescription": "{{name}} právě vystupuje. Převzetí vystoupení odpojí všechny od dané scény.", - "emptyStage": "Nikdo neprezentuje. Vydejte se na pódium a sdílejte prezentaci.", - "stageLoading": "Připojení k pódiu...", - "stageEnded": "Prezentace skončila", - "roomEnded": "Tato schůzka skončila", - "presenterLabel": "{{name}} prezentuje", - "guestLink": "Odkaz pro hosty", - "guestLinkOn": "Odkaz pro hosty je zapnutý. Kdokoli s odkazem může sledovat pódium.", - "guestLinkOff": "Hostovský odkaz je vypnutý.", - "copyLink": "Kopírovat odkaz", - "rotateLink": "Otočit odkaz", - "disableLink": "Zakázat odkaz", - "rotateLinkConfirmTitle": "Otočit odkaz pro hosty?", - "rotateLinkDescription": "Aktuální odkaz okamžitě přestane fungovat. Připojení hosté přes SSH budou odpojeni.", - "disableLinkConfirmTitle": "Zakázat odkaz pro hosty?", - "disableLinkDescription": "Anonymní uživatelé SSH budou okamžitě odpojeni a aktuální odkaz přestane fungovat.", - "linkCopied": "Odkaz zkopírován", - "roles": "Role", - "users": "Uživatelé", - "searchUsers": "Vyhledat uživatele", - "endConfirmTitle": "Ukončit tuto schůzku?", - "endDescription": "{{name}} bude pro všechny ukončeno a nebude možné jej znovu otevřít.", - "endPersistentDescription": "Aktivní prezentace v {{name}} se zastaví. Místnost zůstane k dispozici pro opětovné použití.", - "deleteRoom": "Smazat místnost", - "deleteConfirmTitle": "Smazat tuto místnost?", - "deleteDescription": "{{name}} bude trvale smazáno pro všechny a nelze to vrátit zpět.", - "invitedTo": "Byli jste pozváni na „{{name}}“", - "guest": { - "title": "Zasedání", - "waiting": "Čekání na moderátora...", - "linkInvalid": "Tento odkaz pro hosty je neplatný nebo schůzka skončila." - }, - "reopenFromPanel": "Platnost této karty schůzky vypršela. Znovu otevřete místnost z panelu Schůzky.", - "requestControl": "Požádat o kontrolu", - "cancelControlRequest": "Zrušit požadavek", - "controlQueue": "Žádosti o kontrolu", - "noControlRequests": "Žádné čekající žádosti.", - "grantControlTo": "Předejte kontrolu uživateli {{name}}", - "dismissControlRequest": "Zamítnout požadavek uživatele {{name}}na kontrolu", - "releaseControl": "Ovládání uvolnění", - "grantControl": "Předejte kontrolu", - "revokeControl": "Převezměte zpět kontrolu", - "controlRequestedBy": "{{name}} požádal o kontrolu", - "grant": "Grant", - "controlBadge": "Pod kontrolou", - "youArePresenting": "Prezentujete na jiné kartě. Ukončete prezentaci, abyste mohli předat scénu.", - "created": "Místnost vytvořena", - "invited": "Pozvaní členové", - "roomTab": "Zasedání" - }, "sessionSharing": { - "guestView": { - "loading": "Připojování ke sdílené relaci...", - "linkInvalid": "Tento odkaz pro sdílení je neplatný, jeho platnost vypršela nebo byl zrušen.", - "rateLimited": "Příliš mnoho pokusů, zkuste to prosím znovu brzy", - "sessionEnded": "Tato relace skončila", - "readOnlyBadge": "Pouze zobrazení", - "ownerLabel": "Majitel hostitele" - }, - "modalTitle": "Sdílení relace", - "shareButton": "Podíl", - "notReadyToShare": "Relace ještě není připravena ke sdílení", - "modeTab": { - "link": "Odkaz", - "user": "Uživatel" - }, - "linkModeDescription": "Připojit se může kdokoli s tímto odkazem, účet není potřeba.", - "userModeDescription": "Sdílejte s konkrétním uživatelem, který již má k tomuto hostiteli přístup. Pokud ještě nemá přístup, nejprve s ním hostitele sdílejte nebo použijte odkaz. Po sdílení se relace zobrazí na jeho kartě Připojení.", "permissionLevel": { - "label": "Úroveň oprávnění", "readOnly": "Pouze pro čtení", - "readOnlyDescription": "Může sledovat relaci živě, ale nemůže psát ani interagovat.", - "readWrite": "Čtení a zápis", - "readWriteDescription": "Může psát a interagovat s relací stejně jako její vlastník." - }, - "expiryLabel": "Platnost vypršení platnosti odkazu", - "createLinkButton": "Vytvořit odkaz", - "createShareButton": "Sdílet s uživatelem", - "searchUsersPlaceholder": "Hledat uživatele...", - "noUsersFound": "Nenaleženi žádní uživatelé", - "linkCreated": "Odkaz pro sdílení vytvořen", - "linkCopied": "Odkaz zkopírován do schránky", - "copyLink": "Kopírovat odkaz", - "shareCreated": "Relace sdílena. Zobrazí se na jejich kartě Připojení.", - "shareFailed": "Vytvoření sdílené položky se nezdařilo", - "userLacksHostAccess": "Tento uživatel zatím nemá přístup k tomuto hostiteli. Nejprve s ním hostitele sdílejte nebo použijte odkaz.", - "activeShares": "Aktivní akcie", - "noActiveShares": "Pro tuto relaci nejsou žádné aktivní sdílení", - "revoke": "Zrušit", - "revokeConfirmTitle": "Zrušit toto sdílení?", - "revokeConfirmDescription": "Každý, kdo tuto sdílenou složku používá, okamžitě ztratí přístup.", - "revoked": "Podíl zrušen", - "revokeFailed": "Zrušení sdílení se nezdařilo", - "joinCount": "{{count}} připojit se", - "joinCount_other": "{{count}} se připojí", - "expiresAt": "Platnost vyprší {{date}}", - "linkShareBadge": "Odkaz", - "userShareBadge": "Uživatel: {{username}}", - "loadSharesFailed": "Nepodařilo se načíst aktivní sdílení" + "readWrite": "Čtení a zápis" + } }, "terminal": { "connect": "Připojení k hostiteli", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Odesílá anonymní denní počet uživatelů, hostitelů a využití funkcí, aby pomohl vylepšit Termix. Nikdy neobsahuje žádné osobní údaje ani podrobnosti o připojení.", "analyticsEnabledLockedDesc": "Toto nastavení je uzamčeno proměnnou prostředí ENABLE_TELEMETRY a nelze jej zde změnit.", "updateAnalyticsFailed": "Nepodařilo se aktualizovat nastavení analytických nástrojů", - "sessionSharingGloballyEnabled": "Povolit sdílení relace", "auditForwardingTitle": "Předejte SIEM", "auditForwardingDesc": "Odešlete kopii každého záznamu auditu externímu sběrateli jako NDJSON. Zdrojem pravdivých informací zůstávají lokální protokoly.", "auditForwardingEnvNotice": "Proměnná prostředí také konfiguruje přesměrování; URL uložená zde má přednost.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Token nositele (uložený)", "auditForwardingSaved": "Nastavení přesměrování auditu uloženo", "auditForwardingSaveError": "Nepodařilo se uložit nastavení přesměrování auditu", - "sessionSharingGloballyEnabledDesc": "Povolit sdílení relací živého terminálu, RDP, VNC a Telnet v rámci celé instance. Pokud je tato možnost zakázána, přepíše všechny přepínače sdílení pro jednotlivé hostitele.", - "updateSessionSharingFailed": "Nepodařilo se aktualizovat nastavení sdílení relace", "sessionTimeout": "Časový limit relace", "hours": "hodiny", "sessionTimeoutRange": "Min. 1h · Max. 720h", diff --git a/src/ui/locales/translated/da_DK.json b/src/ui/locales/translated/da_DK.json index 99c02975f..dcef83694 100644 --- a/src/ui/locales/translated/da_DK.json +++ b/src/ui/locales/translated/da_DK.json @@ -667,7 +667,6 @@ "done_cli_link": "Åbn CLI-dokumentationen" }, "nav": { - "collab": "Møder", "home": "Hjem", "terminal": "Terminal", "localTerminal": "Lokal terminal", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Hold din shell og alt, der kører i den, i live på tværs af afbrydelser og genstart af browseren ved at køre den inde i tmux (tmux skal være installeret på værten)", "enableSessionLogging": "Sessionslogning", "enableSessionLoggingDesc": "Optag output fra terminalsessionen til senere gennemgang", - "allowSessionSharing": "Tillad deling af sessioner", - "allowSessionSharingDesc": "Tillad live-sessioner på denne vært at blive delt via link eller med andre brugere", "localEchoLabel": "Lokalt ekko", "localEchoDesc": "Forudsig sikre, udskrivbare tastetryk lokalt på langsomme forbindelser. Adgangskodeprompter og kontrolinput gentages aldrig.", "localEchoDefault": "Brug global indstilling", @@ -1571,137 +1568,11 @@ "join": "Deltag", "sharedSessionLabel": "{{hostName}} (delt)" }, - "collab": { - "createRoom": "Nyt værelse", - "roomName": "Værelsesnavn", - "persistentRoom": "Permanent rum", - "persistentRoomHint": "Permanente rum forbliver på listen efter mødets afslutning og kan genbruges.", - "noRooms": "Ingen rum endnu. Opret et for at starte et møde.", - "openRoom": "Åben", - "endRoom": "Afslut mødet", - "leaveRoom": "Forlade", - "deleteMember": "Fjerne", - "removeMember": "Fjern {{name}}", - "removeMemberTitle": "Fjern dette medlem?", - "removeMemberDescription": "{{name}} mister adgangen til dette rum med det samme. Enhver aktiv SSH-seer vil blive afbrudt.", - "invite": "Invitere", - "inviteTitle": "Inviter medlemmer", - "members": "Medlemmer", - "membersPanel": "Rummedlemmer og kontrolanmodninger", - "membersWithCount": "Medlemmer ({{count}})", - "online": "online", - "offline": "offline", - "you": "(du)", - "hostBadge": "Vært", - "presenterBadge": "Præsentation", - "present": "Nuværende", - "presentTitle": "Indtag scenen", - "presentHost": "Vært", - "searchHosts": "Søg efter værter", - "noHostsFound": "Ingen tilgængelige værter fundet.", - "presentProtocol": "Protokol", - "stopPresenting": "Stop med at præsentere", - "takeOver": "Overtag", - "takeOverConfirmTitle": "Erstat den nuværende præsentation?", - "takeOverDescription": "{{name}} præsenterer nu. Overtagelse vil afbryde alles forbindelse til den scene.", - "emptyStage": "Ingen præsenterer. Gå på scenen for at dele en session.", - "stageLoading": "Forbinder sig til scenen...", - "stageEnded": "Præsentationen sluttede", - "roomEnded": "Dette møde er afsluttet", - "presenterLabel": "{{name}} præsenterer", - "guestLink": "Gæstelink", - "guestLinkOn": "Gæstelinket er aktiveret. Alle med linket kan se scenen.", - "guestLinkOff": "Gæstelinket er slået fra.", - "copyLink": "Kopiér link", - "rotateLink": "Roter link", - "disableLink": "Deaktiver link", - "rotateLinkConfirmTitle": "Roter gæstelinket?", - "rotateLinkDescription": "Det nuværende link vil stoppe med at virke med det samme. Forbundne SSH-gæster vil blive afbrudt.", - "disableLinkConfirmTitle": "Deaktiver gæstelinket?", - "disableLinkDescription": "Anonyme SSH-seere vil blive afbrudt med det samme, og det nuværende link vil stoppe med at virke.", - "linkCopied": "Linket er kopieret", - "roles": "Roller", - "users": "Brugere", - "searchUsers": "Søg brugere", - "endConfirmTitle": "Afslutte dette møde?", - "endDescription": "{{name}} slutter for alle og kan ikke genåbnes.", - "endPersistentDescription": "Den aktive præsentation i {{name}} stopper. Rummet forbliver tilgængeligt til genbrug.", - "deleteRoom": "Slet rum", - "deleteConfirmTitle": "Slet dette rum?", - "deleteDescription": "{{name}} vil blive slettet permanent for alle og kan ikke fortrydes.", - "invitedTo": "Du blev inviteret til \"{{name}}\"", - "guest": { - "title": "Møde", - "waiting": "Venter på præsentatoren...", - "linkInvalid": "Dette gæstelink er ugyldigt, eller mødet er afsluttet" - }, - "reopenFromPanel": "Denne mødefane er udløbet. Genåbn rummet fra panelet Møder.", - "requestControl": "Anmod om kontrol", - "cancelControlRequest": "Annuller anmodning", - "controlQueue": "Kontrolanmodninger", - "noControlRequests": "Ingen ventende anmodninger.", - "grantControlTo": "Giv kontrol til {{name}}", - "dismissControlRequest": "Afvis kontrolanmodningen fra {{name}}", - "releaseControl": "Slipkontrol", - "grantControl": "Giv kontrol", - "revokeControl": "Tag kontrollen tilbage", - "controlRequestedBy": "{{name}} bad om kontrol", - "grant": "Give", - "controlBadge": "I kontrol", - "youArePresenting": "Du præsenterer i en anden fane. Stop præsentationen for at overdrage scenen.", - "created": "Rum oprettet", - "invited": "Medlemmer inviteret", - "roomTab": "Møde" - }, "sessionSharing": { - "guestView": { - "loading": "Forbinder til delt session...", - "linkInvalid": "Dette delingslink er ugyldigt, udløbet eller er blevet tilbagekaldt", - "rateLimited": "For mange forsøg, prøv igen om lidt", - "sessionEnded": "Denne session er afsluttet", - "readOnlyBadge": "Kun visning", - "ownerLabel": "Værtsejer" - }, - "modalTitle": "Del session", - "shareButton": "Dele", - "notReadyToShare": "Sessionen er ikke klar til deling endnu", - "modeTab": { - "link": "Forbindelse", - "user": "Bruger" - }, - "linkModeDescription": "Alle med dette link kan tilmelde sig, ingen konto kræves.", - "userModeDescription": "Del med en bestemt bruger, der allerede har adgang til denne vært. Hvis de ikke har adgang endnu, skal du først dele værten med dem eller bruge et link i stedet. Når sessionen er delt, vises den under deres fane Forbindelser.", "permissionLevel": { - "label": "Tilladelsesniveau", "readOnly": "Skrivebeskyttet", - "readOnlyDescription": "Kan se sessionen live, men kan ikke skrive eller interagere.", - "readWrite": "Læs-skriv", - "readWriteDescription": "Kan skrive og interagere med sessionen ligesom ejeren." - }, - "expiryLabel": "Linkudløb", - "createLinkButton": "Opret link", - "createShareButton": "Del med brugeren", - "searchUsersPlaceholder": "Søg efter brugere...", - "noUsersFound": "Ingen brugere fundet", - "linkCreated": "Delingslink oprettet", - "linkCopied": "Link kopieret til udklipsholder", - "copyLink": "Kopiér link", - "shareCreated": "Session delt. Den vises i fanen Forbindelser.", - "shareFailed": "Kunne ikke oprette deling", - "userLacksHostAccess": "Den bruger har endnu ikke adgang til denne vært. Del værten med dem først, eller brug et link i stedet.", - "activeShares": "Aktive aktier", - "noActiveShares": "Ingen aktive delinger for denne session", - "revoke": "Tilbagekald", - "revokeConfirmTitle": "Tilbagekald denne deling?", - "revokeConfirmDescription": "Enhver, der bruger denne deling, mister adgangen med det samme.", - "revoked": "Deling tilbagekaldt", - "revokeFailed": "Delingen kunne ikke tilbagekaldes", - "joinCount": "{{count}} tilmeld dig", - "joinCount_other": "{{count}} deltager", - "expiresAt": "Udløber {{date}}", - "linkShareBadge": "Forbindelse", - "userShareBadge": "Bruger: {{username}}", - "loadSharesFailed": "Kunne ikke indlæse aktive delinger" + "readWrite": "Læs-skriv" + } }, "terminal": { "connect": "Opret forbindelse til vært", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Sender en anonym daglig optælling af brugere, værter og funktionsbrug for at forbedre Termix. Ingen personlige data eller forbindelsesoplysninger inkluderes nogensinde.", "analyticsEnabledLockedDesc": "Denne indstilling er låst af miljøvariablen ENABLE_TELEMETRY og kan ikke ændres her.", "updateAnalyticsFailed": "Analyseindstillingen kunne ikke opdateres", - "sessionSharingGloballyEnabled": "Tillad deling af sessioner", "auditForwardingTitle": "Videresend til SIEM", "auditForwardingDesc": "Send en kopi af hver revisionspost til en ekstern indsamler som NDJSON. Lokale logfiler forbliver kilden til sandheden.", "auditForwardingEnvNotice": "En miljøvariabel konfigurerer også videresendelse; URL'en, der gemmes her, har forrang.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Bærertoken (gemt)", "auditForwardingSaved": "Indstillinger for videresendelse af revision gemt", "auditForwardingSaveError": "Indstillinger for videresendelse af revision mislykkedes", - "sessionSharingGloballyEnabledDesc": "Tillad deling af live terminal-, RDP-, VNC- og Telnet-sessioner på tværs af instanser. Tilsidesætter alle delingsindstillinger pr. vært, når de er deaktiveret.", - "updateSessionSharingFailed": "Indstillingen for deling af session kunne ikke opdateres", "sessionTimeout": "Sessionstimeout", "hours": "timer", "sessionTimeoutRange": "Min. 1 time · Maks. 720 timer", diff --git a/src/ui/locales/translated/de_DE.json b/src/ui/locales/translated/de_DE.json index 737a0d6f1..2bfb35ff9 100644 --- a/src/ui/locales/translated/de_DE.json +++ b/src/ui/locales/translated/de_DE.json @@ -667,7 +667,6 @@ "done_cli_link": "Öffnen Sie die CLI-Dokumentation." }, "nav": { - "collab": "Treffen", "home": "Startseite", "terminal": "Terminal", "localTerminal": "Lokales Terminal", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Halten Sie Ihre Shell und alle darin laufenden Prozesse auch bei Verbindungsabbrüchen und Browserneustarts am Laufen, indem Sie den Befehl innerhalb von tmux ausführen (tmux muss auf dem Host installiert sein).", "enableSessionLogging": "Sitzungsprotokollierung", "enableSessionLoggingDesc": "Terminal-Sitzungsausgabe zur späteren Überprüfung aufzeichnen", - "allowSessionSharing": "Sitzungsfreigabe zulassen", - "allowSessionSharingDesc": "Ermöglichen Sie es, Live-Sitzungen auf diesem Host per Link oder mit anderen Benutzern zu teilen.", "localEchoLabel": "Lokales Echo", "localEchoDesc": "Bei langsamen Verbindungen werden sichere, druckbare Tastatureingaben lokal vorhergesagt. Passwortabfragen und Steuereingaben werden niemals wiederholt.", "localEchoDefault": "Globale Einstellung verwenden", @@ -1571,137 +1568,11 @@ "join": "Verbinden", "sharedSessionLabel": "{{hostName}} (gemeinsam genutzt)" }, - "collab": { - "createRoom": "Neues Zimmer", - "roomName": "Zimmername", - "persistentRoom": "Persistenter Raum", - "persistentRoomHint": "Die gespeicherten Räume bleiben auch nach Ende des Meetings gelistet und können wiederverwendet werden.", - "noRooms": "Noch keine Räume vorhanden. Erstellen Sie einen, um ein Meeting zu starten.", - "openRoom": "Offen", - "endRoom": "Ende der Besprechung", - "leaveRoom": "Verlassen", - "deleteMember": "Entfernen", - "removeMember": "Entferne {{name}}", - "removeMemberTitle": "Dieses Mitglied entfernen?", - "removeMemberDescription": "{{name}} verliert sofort den Zugriff auf diesen Raum. Alle aktiven SSH-Viewer werden getrennt.", - "invite": "Einladen", - "inviteTitle": "Mitglieder einladen", - "members": "Mitglieder", - "membersPanel": "Raummitglieder und Kontrollanfragen", - "membersWithCount": "Mitglieder ({{count}})", - "online": "online", - "offline": "offline", - "you": "(Du)", - "hostBadge": "Gastgeber", - "presenterBadge": "Präsentation", - "present": "Gegenwärtig", - "presentTitle": "Betretet die Bühne", - "presentHost": "Gastgeber", - "searchHosts": "Suchmaschinen", - "noHostsFound": "Es wurden keine verfügbaren Hosts gefunden.", - "presentProtocol": "Protokoll", - "stopPresenting": "Hör auf zu präsentieren", - "takeOver": "Übernehmen", - "takeOverConfirmTitle": "Die aktuelle Präsentation ersetzen?", - "takeOverDescription": "{{name}} präsentiert sich jetzt. Die Übernahme trennt alle von dieser Bühne.", - "emptyStage": "Niemand präsentiert. Ergreife die Bühne, um eine Session zu teilen.", - "stageLoading": "Verbindung zur Bühne...", - "stageEnded": "Die Präsentation endete", - "roomEnded": "Diese Sitzung ist beendet.", - "presenterLabel": "{{name}} präsentiert", - "guestLink": "Gastlink", - "guestLinkOn": "Der Gastzugang ist aktiviert. Jeder mit dem Link kann die Bühnenshow verfolgen.", - "guestLinkOff": "Der Gastlink ist deaktiviert.", - "copyLink": "Link kopieren", - "rotateLink": "Drehverbindung", - "disableLink": "Link deaktivieren", - "rotateLinkConfirmTitle": "Den Gastlink drehen?", - "rotateLinkDescription": "Die bestehende Verbindung wird sofort unterbrochen. Verbundene SSH-Gäste werden getrennt.", - "disableLinkConfirmTitle": "Gastlink deaktivieren?", - "disableLinkDescription": "Anonyme SSH-Zugriffe werden sofort getrennt und die aktuelle Verbindung funktioniert nicht mehr.", - "linkCopied": "Link kopiert", - "roles": "Rollen", - "users": "Benutzer", - "searchUsers": "Nutzer suchen", - "endConfirmTitle": "Diese Sitzung beenden?", - "endDescription": "{{name}} wird für alle beendet und kann nicht wieder geöffnet werden.", - "endPersistentDescription": "Die aktive Präsentation in {{name}} wird beendet. Der Raum steht weiterhin zur erneuten Verwendung zur Verfügung.", - "deleteRoom": "Raum löschen", - "deleteConfirmTitle": "Diesen Raum löschen?", - "deleteDescription": "{{name}} wird für alle dauerhaft gelöscht und kann nicht rückgängig gemacht werden.", - "invitedTo": "Sie wurden eingeladen zu \"{{name}}\"", - "guest": { - "title": "Treffen", - "waiting": "Ich warte auf den Moderator...", - "linkInvalid": "Dieser Gastlink ist ungültig oder das Meeting ist beendet." - }, - "reopenFromPanel": "Dieser Besprechungsreiter ist abgelaufen. Öffnen Sie den Raum erneut über das Besprechungsmenü.", - "requestControl": "Anforderungskontrolle", - "cancelControlRequest": "Stornierungsanfrage", - "controlQueue": "Kontrollanfragen", - "noControlRequests": "Keine offenen Anfragen.", - "grantControlTo": "Übergib die Kontrolle an {{name}}", - "dismissControlRequest": "Kontrollanfrage von {{name}}ablehnen", - "releaseControl": "Freigabekontrolle", - "grantControl": "Kontrolle abgeben", - "revokeControl": "Übernehmen Sie wieder die Kontrolle", - "controlRequestedBy": "{{name}} hat um Kontrolle gebeten", - "grant": "Gewähren", - "controlBadge": "Unter Kontrolle", - "youArePresenting": "Sie präsentieren in einem anderen Tab. Beenden Sie Ihre Präsentation, um die Bühne zu übergeben.", - "created": "Raum geschaffen", - "invited": "Mitglieder eingeladen", - "roomTab": "Treffen" - }, "sessionSharing": { - "guestView": { - "loading": "Verbindung zur gemeinsamen Sitzung wird hergestellt...", - "linkInvalid": "Dieser Freigabelink ist ungültig, abgelaufen oder wurde widerrufen.", - "rateLimited": "Zu viele Versuche, bitte versuchen Sie es in Kürze erneut.", - "sessionEnded": "Diese Sitzung ist beendet.", - "readOnlyBadge": "Nur Ansicht", - "ownerLabel": "Gastgeber" - }, - "modalTitle": "Sitzung teilen", - "shareButton": "Aktie", - "notReadyToShare": "Die Sitzung ist noch nicht zur Freigabe bereit.", - "modeTab": { - "link": "Link", - "user": "Benutzer" - }, - "linkModeDescription": "Jeder mit diesem Link kann beitreten, ein Konto ist nicht erforderlich.", - "userModeDescription": "Teilen Sie die Sitzung mit einem bestimmten Benutzer, der bereits Zugriff auf diesen Host hat. Falls er noch keinen Zugriff hat, teilen Sie ihm den Host zuerst mit oder verwenden Sie stattdessen einen Link. Nach der Freigabe erscheint die Sitzung im Tab „Verbindungen“ des Benutzers.", "permissionLevel": { - "label": "Berechtigungsstufe", "readOnly": "Nur lesbar", - "readOnlyDescription": "Ich kann die Sitzung live verfolgen, aber nicht tippen oder interagieren.", - "readWrite": "Lesen und Schreiben", - "readWriteDescription": "Kann in der Sitzung genauso tippen und mit ihr interagieren wie der Besitzer." - }, - "expiryLabel": "Linkablauf", - "createLinkButton": "Verknüpfung erstellen", - "createShareButton": "Mit dem Nutzer teilen", - "searchUsersPlaceholder": "Benutzer suchen...", - "noUsersFound": "Keine Benutzer gefunden", - "linkCreated": "Link zum Teilen erstellt", - "linkCopied": "Link in die Zwischenablage kopiert", - "copyLink": "Link kopieren", - "shareCreated": "Die Sitzung wurde freigegeben. Sie wird im Tab „Verbindungen“ des Benutzers angezeigt.", - "shareFailed": "Fehler beim Erstellen der Freigabe", - "userLacksHostAccess": "Dieser Benutzer hat noch keinen Zugriff auf diesen Host. Teilen Sie ihm den Host zuerst mit oder verwenden Sie stattdessen einen Link.", - "activeShares": "Aktive Anteile", - "noActiveShares": "Keine aktiven Anteile für diese Sitzung", - "revoke": "Widerrufen", - "revokeConfirmTitle": "Diese Aktie widerrufen?", - "revokeConfirmDescription": "Wer diese Freigabe nutzt, verliert sofort den Zugriff.", - "revoked": "Anteil widerrufen", - "revokeFailed": "Aktienwiderruf fehlgeschlagen", - "joinCount": "{{count}} beitreten", - "joinCount_other": "{{count}} verbindet sich", - "expiresAt": "Läuft ab {{date}}", - "linkShareBadge": "Link", - "userShareBadge": "Benutzer: {{username}}", - "loadSharesFailed": "Fehler beim Laden der aktiven Anteile" + "readWrite": "Lesen und Schreiben" + } }, "terminal": { "connect": "Mit Host verbinden", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Sendet täglich anonyme Zählungen von Nutzern, Hosts und Funktionsnutzung, um Termix zu verbessern. Es werden niemals personenbezogene Daten oder Verbindungsdaten übermittelt.", "analyticsEnabledLockedDesc": "Diese Einstellung ist durch die Umgebungsvariable ENABLE_TELEMETRY gesperrt und kann hier nicht geändert werden.", "updateAnalyticsFailed": "Die Aktualisierung der Analyseeinstellungen ist fehlgeschlagen.", - "sessionSharingGloballyEnabled": "Sitzungsfreigabe zulassen", "auditForwardingTitle": "Weiterleitung an SIEM", "auditForwardingDesc": "Senden Sie eine Kopie jedes Prüfeintrags als NDJSON an einen externen Datensammler. Lokale Protokolle bleiben die maßgebliche Datenquelle.", "auditForwardingEnvNotice": "Eine Umgebungsvariable konfiguriert auch die Weiterleitung; die hier gespeicherte URL hat Vorrang.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Bearer-Token (gespeichert)", "auditForwardingSaved": "Einstellungen für die Auditweiterleitung gespeichert", "auditForwardingSaveError": "Die Einstellungen für die Auditweiterleitung konnten nicht gespeichert werden.", - "sessionSharingGloballyEnabledDesc": "Ermöglicht die instanzweite Freigabe von Live-Terminal-, RDP-, VNC- und Telnet-Sitzungen. Überschreibt alle Freigabeeinstellungen pro Host, wenn diese deaktiviert sind.", - "updateSessionSharingFailed": "Fehler beim Aktualisieren der Sitzungsfreigabeeinstellung", "sessionTimeout": "Sitzungszeitlimit", "hours": "Stunden", "sessionTimeoutRange": "Min. 1 Std. · Max. 720 Std.", diff --git a/src/ui/locales/translated/el_GR.json b/src/ui/locales/translated/el_GR.json index ebf87901f..3ba876577 100644 --- a/src/ui/locales/translated/el_GR.json +++ b/src/ui/locales/translated/el_GR.json @@ -667,7 +667,6 @@ "done_cli_link": "Άνοιγμα εγγράφων CLI" }, "nav": { - "collab": "Συναντήσεις", "home": "Σπίτι", "terminal": "Τερματικό", "localTerminal": "Τοπικό τερματικό", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Διατηρήστε το κέλυφος (shell) και οτιδήποτε εκτελείται σε αυτό ενεργό σε αποσυνδέσεις και επανεκκινήσεις του προγράμματος περιήγησης, εκτελώντας το μέσα στο tmux (το tmux πρέπει να είναι εγκατεστημένο στον κεντρικό υπολογιστή)", "enableSessionLogging": "Καταγραφή συνεδρίας", "enableSessionLoggingDesc": "Καταγραφή εξόδου συνεδρίας τερματικού για μελλοντική αναθεώρηση", - "allowSessionSharing": "Επιτρέψτε την κοινή χρήση περιόδου σύνδεσης", - "allowSessionSharingDesc": "Να επιτρέπεται η κοινοποίηση ζωντανών συνεδριών σε αυτόν τον πάροχο μέσω συνδέσμου ή με άλλους χρήστες", "localEchoLabel": "Τοπική Ηχώ", "localEchoDesc": "Προβλέψτε ασφαλείς εκτυπώσιμες πληκτρολογήσεις τοπικά σε αργές συνδέσεις. Οι προτροπές κωδικού πρόσβασης και η εισαγωγή ελέγχου δεν επαναλαμβάνονται ποτέ.", "localEchoDefault": "Χρήση καθολικής ρύθμισης", @@ -1571,137 +1568,11 @@ "join": "Ενώνω", "sharedSessionLabel": "{{hostName}} (κοινόχρηστο)" }, - "collab": { - "createRoom": "Νέο δωμάτιο", - "roomName": "Όνομα δωματίου", - "persistentRoom": "Μόνιμο δωμάτιο", - "persistentRoomHint": "Οι μόνιμες αίθουσες παραμένουν καταχωρημένες μετά το τέλος της σύσκεψης και μπορούν να επαναχρησιμοποιηθούν.", - "noRooms": "Δεν υπάρχουν ακόμη αίθουσες. Δημιουργήστε μία για να ξεκινήσετε μια σύσκεψη.", - "openRoom": "Ανοιχτό", - "endRoom": "Λήξη συνάντησης", - "leaveRoom": "Αδεια", - "deleteMember": "Αφαιρώ", - "removeMember": "Αφαίρεση {{name}}", - "removeMemberTitle": "Κατάργηση αυτού του μέλους;", - "removeMemberDescription": "Ο χρήστης {{name}} θα χάσει αμέσως την πρόσβαση σε αυτό το δωμάτιο. Οποιοσδήποτε ενεργός χρήστης προβολής SSH θα αποσυνδεθεί.", - "invite": "Καλώ", - "inviteTitle": "Πρόσκληση μελών", - "members": "Μέλη", - "membersPanel": "Μέλη δωματίου και αιτήματα ελέγχου", - "membersWithCount": "Μέλη ({{count}})", - "online": "διαδικτυακά", - "offline": "εκτός σύνδεσης", - "you": "(εσείς)", - "hostBadge": "Πλήθος", - "presenterBadge": "Παρουσίαση", - "present": "Παρόν", - "presentTitle": "Ανέβα στη σκηνή", - "presentHost": "Πλήθος", - "searchHosts": "Αναζήτηση κεντρικών υπολογιστών", - "noHostsFound": "Δεν βρέθηκαν διαθέσιμοι κεντρικοί υπολογιστές.", - "presentProtocol": "Πρωτόκολλο", - "stopPresenting": "Διακοπή παρουσίασης", - "takeOver": "Αναλαμβάνω", - "takeOverConfirmTitle": "Αντικατάσταση της τρέχουσας παρουσίασης;", - "takeOverDescription": "Το {{name}} παρουσιάζεται τώρα. Η ανάληψη του ελέγχου θα αποσυνδέσει όλους από αυτό το στάδιο.", - "emptyStage": "Κανείς δεν κάνει παρουσίαση. Ανεβείτε στη σκηνή για να μοιραστείτε μια συνεδρία.", - "stageLoading": "Σύνδεση με τη σκηνή...", - "stageEnded": "Η παρουσίαση έληξε", - "roomEnded": "Αυτή η συνάντηση έληξε", - "presenterLabel": "{{name}} παρουσιάζει", - "guestLink": "Σύνδεσμος επισκέπτη", - "guestLinkOn": "Ο σύνδεσμος επισκεπτών είναι ενεργοποιημένος. Όποιος έχει τον σύνδεσμο μπορεί να παρακολουθήσει τη σκηνή.", - "guestLinkOff": "Ο σύνδεσμος επισκέπτη είναι απενεργοποιημένος.", - "copyLink": "Αντιγραφή συνδέσμου", - "rotateLink": "Περιστροφή συνδέσμου", - "disableLink": "Απενεργοποίηση συνδέσμου", - "rotateLinkConfirmTitle": "Εναλλαγή του συνδέσμου επισκέπτη;", - "rotateLinkDescription": "Ο τρέχων σύνδεσμος θα σταματήσει να λειτουργεί αμέσως. Οι συνδεδεμένοι επισκέπτες SSH θα αποσυνδεθούν.", - "disableLinkConfirmTitle": "Απενεργοποίηση του συνδέσμου επισκέπτη;", - "disableLinkDescription": "Οι ανώνυμοι θεατές SSH θα αποσυνδεθούν αμέσως και ο τρέχων σύνδεσμος θα σταματήσει να λειτουργεί.", - "linkCopied": "Ο σύνδεσμος αντιγράφηκε", - "roles": "Ρόλοι", - "users": "Χρήστες", - "searchUsers": "Αναζήτηση χρηστών", - "endConfirmTitle": "Λήξη αυτής της συνάντησης;", - "endDescription": "Το {{name}} θα λήξει για όλους και δεν μπορεί να ανοίξει ξανά.", - "endPersistentDescription": "Η ενεργή παρουσίαση στο {{name}} θα σταματήσει. Η αίθουσα παραμένει διαθέσιμη για επαναχρησιμοποίηση.", - "deleteRoom": "Διαγραφή δωματίου", - "deleteConfirmTitle": "Διαγραφή αυτού του δωματίου;", - "deleteDescription": "Η εντολή {{name}} θα διαγραφεί οριστικά για όλους και δεν θα είναι δυνατή η αναίρεση της.", - "invitedTo": "Σας προσκάλεσαν στο \"{{name}}\"", - "guest": { - "title": "Συνάντηση", - "waiting": "Περιμένοντας τον παρουσιαστή...", - "linkInvalid": "Αυτός ο σύνδεσμος επισκέπτη δεν είναι έγκυρος ή η σύσκεψη έχει λήξει" - }, - "reopenFromPanel": "Αυτή η καρτέλα σύσκεψης έληξε. Ανοίξτε ξανά την αίθουσα από τον πίνακα \"Συνέδρια\".", - "requestControl": "Αίτημα ελέγχου", - "cancelControlRequest": "Αίτημα ακύρωσης", - "controlQueue": "Αιτήματα ελέγχου", - "noControlRequests": "Δεν υπάρχουν εκκρεμή αιτήματα.", - "grantControlTo": "Δώστε τον έλεγχο σε {{name}}", - "dismissControlRequest": "Απόρριψη αιτήματος ελέγχου του {{name}}", - "releaseControl": "Έλεγχος απελευθέρωσης", - "grantControl": "Δώστε τον έλεγχο", - "revokeControl": "Πάρε πίσω τον έλεγχο", - "controlRequestedBy": "{{name}} ζήτησε έλεγχο", - "grant": "Χορήγηση", - "controlBadge": "Υπό έλεγχο", - "youArePresenting": "Κάνετε παρουσίαση σε άλλη καρτέλα. Διακόψτε την παρουσίαση για να παραδώσετε τη σκηνή.", - "created": "Δημιουργήθηκε δωμάτιο", - "invited": "Μέλη που έχουν προσκληθεί", - "roomTab": "Συνάντηση" - }, "sessionSharing": { - "guestView": { - "loading": "Σύνδεση σε κοινόχρηστη συνεδρία...", - "linkInvalid": "Αυτός ο σύνδεσμος κοινής χρήσης δεν είναι έγκυρος, έχει λήξει ή έχει ανακληθεί", - "rateLimited": "Πάρα πολλές προσπάθειες, δοκιμάστε ξανά σύντομα.", - "sessionEnded": "Αυτή η συνεδρία έληξε", - "readOnlyBadge": "Μόνο προβολή", - "ownerLabel": "Κάτοχος κεντρικού υπολογιστή" - }, - "modalTitle": "Κοινή χρήση περιόδου σύνδεσης", - "shareButton": "Μερίδιο", - "notReadyToShare": "Η συνεδρία δεν είναι ακόμη έτοιμη για κοινή χρήση", - "modeTab": { - "link": "Σύνδεσμος", - "user": "Μεταχειριζόμενος" - }, - "linkModeDescription": "Οποιοσδήποτε έχει αυτόν τον σύνδεσμο μπορεί να συμμετάσχει, δεν απαιτείται λογαριασμός.", - "userModeDescription": "Κοινοποίηση με έναν συγκεκριμένο χρήστη που έχει ήδη πρόσβαση σε αυτόν τον κεντρικό υπολογιστή. Εάν δεν έχει ακόμη πρόσβαση, μοιραστείτε πρώτα τον κεντρικό υπολογιστή μαζί του ή χρησιμοποιήστε έναν σύνδεσμο. Μόλις κοινοποιηθεί, η συνεδρία εμφανίζεται στην καρτέλα Συνδέσεις.", "permissionLevel": { - "label": "Επίπεδο δικαιωμάτων", "readOnly": "Μόνο για ανάγνωση", - "readOnlyDescription": "Μπορώ να παρακολουθήσω την συνεδρία ζωντανά, αλλά δεν μπορώ να πληκτρολογήσω ή να αλληλεπιδράσω.", - "readWrite": "Ανάγνωση-εγγραφή", - "readWriteDescription": "Μπορεί να πληκτρολογήσει και να αλληλεπιδράσει με την περίοδο λειτουργίας όπως ο κάτοχος." - }, - "expiryLabel": "Λήξη σύνδεσης", - "createLinkButton": "Δημιουργία συνδέσμου", - "createShareButton": "Κοινοποίηση με χρήστη", - "searchUsersPlaceholder": "Αναζήτηση χρηστών...", - "noUsersFound": "Δεν βρέθηκαν χρήστες", - "linkCreated": "Ο σύνδεσμος κοινής χρήσης δημιουργήθηκε", - "linkCopied": "Ο σύνδεσμος αντιγράφηκε στο πρόχειρο", - "copyLink": "Αντιγραφή συνδέσμου", - "shareCreated": "Η συνεδρία κοινοποιήθηκε. Θα εμφανιστεί στην καρτέλα \"Συνδέσεις\".", - "shareFailed": "Η δημιουργία κοινής χρήσης απέτυχε", - "userLacksHostAccess": "Αυτός ο χρήστης δεν έχει ακόμη πρόσβαση σε αυτόν τον κεντρικό υπολογιστή. Κοινοποιήστε πρώτα τον κεντρικό υπολογιστή μαζί του ή χρησιμοποιήστε έναν σύνδεσμο.", - "activeShares": "Ενεργές κοινοποιήσεις", - "noActiveShares": "Δεν υπάρχουν ενεργές κοινοποιήσεις για αυτήν την περίοδο σύνδεσης", - "revoke": "Ανακαλώ", - "revokeConfirmTitle": "Ανάκληση αυτής της κοινοποίησης;", - "revokeConfirmDescription": "Όποιος χρησιμοποιεί αυτήν την κοινοποίηση θα χάσει αμέσως την πρόσβαση.", - "revoked": "Η κοινοποίηση ανακλήθηκε", - "revokeFailed": "Η ανάκληση της κοινής χρήσης απέτυχε", - "joinCount": "{{count}} συμμετοχή", - "joinCount_other": "{{count}} ενώσεις", - "expiresAt": "Λήγει {{date}}", - "linkShareBadge": "Σύνδεσμος", - "userShareBadge": "Χρήστης: {{username}}", - "loadSharesFailed": "Αποτυχία φόρτωσης ενεργών κοινόχρηστων στοιχείων" + "readWrite": "Ανάγνωση-εγγραφή" + } }, "terminal": { "connect": "Σύνδεση με τον κεντρικό υπολογιστή", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Αποστέλλει έναν ανώνυμο ημερήσιο αριθμό χρηστών, κεντρικών υπολογιστών και χρήσης λειτουργιών για να βοηθήσει στη βελτίωση του Termix. Δεν περιλαμβάνονται ποτέ προσωπικά δεδομένα ή λεπτομέρειες σύνδεσης.", "analyticsEnabledLockedDesc": "Αυτή η ρύθμιση είναι κλειδωμένη από τη μεταβλητή περιβάλλοντος ENABLE_TELEMETRY και δεν μπορεί να αλλάξει εδώ.", "updateAnalyticsFailed": "Η ενημέρωση της ρύθμισης αναλυτικών στοιχείων απέτυχε", - "sessionSharingGloballyEnabled": "Επιτρέψτε την κοινή χρήση περιόδου σύνδεσης", "auditForwardingTitle": "Προώθηση στο SIEM", "auditForwardingDesc": "Στείλτε ένα αντίγραφο κάθε καταχώρησης ελέγχου σε έναν εξωτερικό συλλέκτη ως NDJSON. Τα τοπικά αρχεία καταγραφής παραμένουν η πηγή της αλήθειας.", "auditForwardingEnvNotice": "Μια μεταβλητή περιβάλλοντος ρυθμίζει επίσης την προώθηση. Η διεύθυνση URL που αποθηκεύεται εδώ έχει προτεραιότητα.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Διακριτικό κομιστή (αποθηκευμένο)", "auditForwardingSaved": "Οι ρυθμίσεις προώθησης ελέγχου αποθηκεύτηκαν", "auditForwardingSaveError": "Αποτυχία αποθήκευσης ρυθμίσεων προώθησης ελέγχου", - "sessionSharingGloballyEnabledDesc": "Επιτρέπεται η κοινή χρήση των ζωντανών συνεδριών τερματικού, RDP, VNC και Telnet σε ολόκληρη την παρουσία. Παρακάμπτει κάθε εναλλαγή κοινής χρήσης ανά κεντρικό υπολογιστή όταν είναι απενεργοποιημένη.", - "updateSessionSharingFailed": "Αποτυχία ενημέρωσης ρύθμισης κοινής χρήσης περιόδου σύνδεσης", "sessionTimeout": "Χρονικό όριο συνεδρίας", "hours": "ώρες", "sessionTimeoutRange": "Ελάχ. 1 ώρα · Μέγ. 720 ώρες", diff --git a/src/ui/locales/translated/es_ES.json b/src/ui/locales/translated/es_ES.json index c78f61e0e..96b8793ff 100644 --- a/src/ui/locales/translated/es_ES.json +++ b/src/ui/locales/translated/es_ES.json @@ -667,7 +667,6 @@ "done_cli_link": "Abre la documentación de la CLI." }, "nav": { - "collab": "Reuniones", "home": "Inicio", "terminal": "Terminal", "localTerminal": "Terminal local", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Mantén tu shell y todo lo que se esté ejecutando en él activo durante desconexiones y reinicios del navegador ejecutándolo dentro de tmux (tmux debe estar instalado en el host).", "enableSessionLogging": "Registro de sesión", "enableSessionLoggingDesc": "Registrar la salida de la sesión de terminal para su revisión posterior", - "allowSessionSharing": "Permitir compartir la sesión", - "allowSessionSharingDesc": "Permitir que las sesiones en vivo en este servidor se compartan mediante un enlace o con otros usuarios.", "localEchoLabel": "Eco local", "localEchoDesc": "Predice localmente las pulsaciones de teclas seguras para imprimir en conexiones lentas. Las solicitudes de contraseña y la entrada de control nunca se repiten.", "localEchoDefault": "Utilizar la configuración global", @@ -1571,137 +1568,11 @@ "join": "Unirse", "sharedSessionLabel": "{{hostName}} (compartido)" }, - "collab": { - "createRoom": "Habitación nueva", - "roomName": "Nombre de la habitación", - "persistentRoom": "Habitación persistente", - "persistentRoomHint": "Las salas reservadas permanecen en la lista una vez finalizada la reunión y pueden volver a utilizarse.", - "noRooms": "Aún no hay salas disponibles. Crea una para iniciar una reunión.", - "openRoom": "Abierto", - "endRoom": "Fin de la reunión", - "leaveRoom": "Dejar", - "deleteMember": "Eliminar", - "removeMember": "Eliminar {{name}}", - "removeMemberTitle": "¿Eliminar a este miembro?", - "removeMemberDescription": "{{name}} perderá el acceso a esta sala inmediatamente. Cualquier visor SSH activo será desconectado.", - "invite": "Invitar", - "inviteTitle": "Invitar a los miembros", - "members": "Miembros", - "membersPanel": "Miembros de la sala y solicitudes de control", - "membersWithCount": "Miembros ({{count}})", - "online": "en línea", - "offline": "desconectado", - "you": "(tú)", - "hostBadge": "Anfitrión", - "presenterBadge": "Presentación", - "present": "Presente", - "presentTitle": "Sube al escenario", - "presentHost": "Anfitrión", - "searchHosts": "Buscar hosts", - "noHostsFound": "No se encontraron hosts disponibles.", - "presentProtocol": "Protocolo", - "stopPresenting": "Deja de presentar", - "takeOver": "Tomar el control", - "takeOverConfirmTitle": "¿Reemplazar la presentación actual?", - "takeOverDescription": "{{name}} se está presentando ahora. Tomar el control desconectará a todos de ese escenario.", - "emptyStage": "Nadie va a presentar. Sube al escenario para compartir una sesión.", - "stageLoading": "Conectando con el escenario...", - "stageEnded": "La presentación terminó", - "roomEnded": "Esta reunión ha terminado.", - "presenterLabel": "{{name}} está presentando", - "guestLink": "Enlace de invitado", - "guestLinkOn": "El enlace para invitados está activado. Cualquiera que tenga el enlace puede ver el espectáculo.", - "guestLinkOff": "El enlace de invitado está desactivado.", - "copyLink": "Copiar enlace", - "rotateLink": "Enlace giratorio", - "disableLink": "Deshabilitar enlace", - "rotateLinkConfirmTitle": "¿Rotar el enlace de invitado?", - "rotateLinkDescription": "El enlace actual dejará de funcionar inmediatamente. Los invitados SSH conectados serán desconectados.", - "disableLinkConfirmTitle": "¿Deshabilitar el enlace de invitado?", - "disableLinkDescription": "Los usuarios que intenten acceder de forma anónima mediante SSH serán desconectados inmediatamente y el enlace actual dejará de funcionar.", - "linkCopied": "Enlace copiado", - "roles": "Roles", - "users": "Usuarios", - "searchUsers": "Buscar usuarios", - "endConfirmTitle": "¿Dar por terminada esta reunión?", - "endDescription": "{{name}} finalizará para todos y no podrá reabrirse.", - "endPersistentDescription": "La presentación activa en {{name}} se detendrá. La sala permanece disponible para su reutilización.", - "deleteRoom": "Eliminar habitación", - "deleteConfirmTitle": "¿Eliminar esta habitación?", - "deleteDescription": "{{name}} se eliminará permanentemente para todos y no se podrá deshacer.", - "invitedTo": "Te invitaron a \"{{name}}\"", - "guest": { - "title": "Reunión", - "waiting": "Esperando al presentador...", - "linkInvalid": "Este enlace de invitado no es válido o la reunión ha finalizado." - }, - "reopenFromPanel": "Esta pestaña de reunión ha caducado. Vuelva a abrir la sala desde el panel de Reuniones.", - "requestControl": "Control de solicitudes", - "cancelControlRequest": "Cancelar solicitud", - "controlQueue": "Solicitudes de control", - "noControlRequests": "No hay solicitudes pendientes.", - "grantControlTo": "Cede el control a {{name}}", - "dismissControlRequest": "Rechazar la solicitud de control de {{name}}", - "releaseControl": "Control de liberación", - "grantControl": "Cede el control", - "revokeControl": "Recupera el control", - "controlRequestedBy": "{{name}} pidió control", - "grant": "Conceder", - "controlBadge": "En control", - "youArePresenting": "Estás presentando en otra pestaña. Deja de presentar para ceder la palabra.", - "created": "Habitación creada", - "invited": "Miembros invitados", - "roomTab": "Reunión" - }, "sessionSharing": { - "guestView": { - "loading": "Conectando con la sesión compartida...", - "linkInvalid": "Este enlace para compartir no es válido, ha caducado o ha sido revocado.", - "rateLimited": "Demasiados intentos, por favor inténtelo de nuevo en breve.", - "sessionEnded": "Esta sesión ha finalizado.", - "readOnlyBadge": "Solo lectura", - "ownerLabel": "Propietario del anfitrión" - }, - "modalTitle": "Compartir sesión", - "shareButton": "Compartir", - "notReadyToShare": "La sesión aún no está lista para compartirse.", - "modeTab": { - "link": "Enlace", - "user": "Usuario" - }, - "linkModeDescription": "Cualquier persona con este enlace puede unirse, no se requiere cuenta.", - "userModeDescription": "Comparte la sesión con un usuario específico que ya tenga acceso a este host. Si aún no tiene acceso, comparte primero el host con él o utiliza un enlace. Una vez compartida, la sesión aparecerá en su pestaña Conexiones.", "permissionLevel": { - "label": "Nivel de permiso", "readOnly": "Solo lectura", - "readOnlyDescription": "Se puede ver la sesión en directo, pero no se puede escribir ni interactuar.", - "readWrite": "Leer y escribir", - "readWriteDescription": "Puede escribir e interactuar con la sesión igual que el propietario." - }, - "expiryLabel": "Caducidad del enlace", - "createLinkButton": "Crear enlace", - "createShareButton": "Compartir con el usuario", - "searchUsersPlaceholder": "Buscar usuarios...", - "noUsersFound": "No se encontraron usuarios", - "linkCreated": "Enlace para compartir creado", - "linkCopied": "Enlace copiado al portapapeles", - "copyLink": "Copiar enlace", - "shareCreated": "Sesión compartida. Aparecerá en su pestaña Conexiones.", - "shareFailed": "No se pudo crear el recurso compartido", - "userLacksHostAccess": "Ese usuario aún no tiene acceso a este servidor. Primero, comparte el servidor con él o utiliza un enlace.", - "activeShares": "Acciones activas", - "noActiveShares": "No hay acciones activas para esta sesión.", - "revoke": "Revocar", - "revokeConfirmTitle": "¿Revocar esta acción?", - "revokeConfirmDescription": "Cualquier persona que utilice este recurso compartido perderá el acceso inmediatamente.", - "revoked": "Acción revocada", - "revokeFailed": "No se pudo revocar la participación.", - "joinCount": "{{count}} unirse", - "joinCount_other": "{{count}} se une", - "expiresAt": "Caduca {{date}}", - "linkShareBadge": "Enlace", - "userShareBadge": "Usuario: {{username}}", - "loadSharesFailed": "No se pudieron cargar los recursos compartidos activos." + "readWrite": "Leer y escribir" + } }, "terminal": { "connect": "Conectar al host", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Envía un recuento diario anónimo de usuarios, hosts y uso de funciones para ayudar a mejorar Termix. No se incluyen datos personales ni detalles de conexión.", "analyticsEnabledLockedDesc": "Esta configuración está bloqueada por la variable de entorno ENABLE_TELEMETRY y no se puede cambiar aquí.", "updateAnalyticsFailed": "No se pudo actualizar la configuración de análisis.", - "sessionSharingGloballyEnabled": "Permitir compartir la sesión", "auditForwardingTitle": "Reenviar a SIEM", "auditForwardingDesc": "Envíe una copia de cada entrada de auditoría a un recopilador externo en formato NDJSON. Los registros locales siguen siendo la fuente de información fidedigna.", "auditForwardingEnvNotice": "Una variable de entorno también configura el reenvío; la URL guardada aquí tiene prioridad.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Token de portador (almacenado)", "auditForwardingSaved": "Configuración de reenvío de auditoría guardada", "auditForwardingSaveError": "No se pudieron guardar los ajustes de reenvío de auditoría.", - "sessionSharingGloballyEnabledDesc": "Permite compartir sesiones de terminal en vivo, RDP, VNC y Telnet en toda la instancia. Cuando está desactivado, anula cualquier configuración de uso compartido por host.", - "updateSessionSharingFailed": "No se pudo actualizar la configuración de uso compartido de la sesión.", "sessionTimeout": "Tiempo de expiración de sesión", "hours": "horas", "sessionTimeoutRange": "Mín 1h · Máx 720h", diff --git a/src/ui/locales/translated/fi_FI.json b/src/ui/locales/translated/fi_FI.json index 80465ac9c..ce07ef8e0 100644 --- a/src/ui/locales/translated/fi_FI.json +++ b/src/ui/locales/translated/fi_FI.json @@ -667,7 +667,6 @@ "done_cli_link": "Avaa CLI-dokumentaatio" }, "nav": { - "collab": "Kokoukset", "home": "Kotiin", "terminal": "Terminaali", "localTerminal": "Paikallinen terminaali", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Pidä komentotulkkisi ja kaikki siinä käynnissä olevat toiminnot toiminnassa yhteyskatkosten ja selaimen uudelleenkäynnistysten aikana suorittamalla se tmuxin sisällä (tmuxin on oltava asennettuna isäntäkoneelle)", "enableSessionLogging": "Istuntojen lokitiedot", "enableSessionLoggingDesc": "Tallenna pääteistunnon tuloste myöhempää tarkastelua varten", - "allowSessionSharing": "Salli istunnon jakaminen", - "allowSessionSharingDesc": "Salli live-istuntojen jakaminen tällä isännällä linkin kautta tai muiden käyttäjien kanssa", "localEchoLabel": "Paikallinen kaiku", "localEchoDesc": "Ennusta turvalliset tulostettavat näppäinpainallukset paikallisesti hitailla yhteyksillä. Salasanakehotteita ja ohjaussyötettä ei koskaan toisteta.", "localEchoDefault": "Käytä yleistä asetusta", @@ -1571,137 +1568,11 @@ "join": "Liity", "sharedSessionLabel": "{{hostName}} (jaettu)" }, - "collab": { - "createRoom": "Uusi huone", - "roomName": "Huoneen nimi", - "persistentRoom": "Pysyvä huone", - "persistentRoomHint": "Pysyvät huoneet pysyvät luettelossa kokouksen päättymisen jälkeen ja niitä voidaan käyttää uudelleen.", - "noRooms": "Ei vielä huoneita. Luo huone aloittaaksesi kokouksen.", - "openRoom": "Avata", - "endRoom": "Kokouksen loppu", - "leaveRoom": "Lähtö", - "deleteMember": "Poistaa", - "removeMember": "Poista {{name}}", - "removeMemberTitle": "Poistetaanko tämä jäsen?", - "removeMemberDescription": "{{name}} menettää pääsyn tähän huoneeseen välittömästi. Kaikki aktiiviset SSH-katseluohjelmat katkaistaan.", - "invite": "Kutsu", - "inviteTitle": "Kutsu jäseniä", - "members": "Jäsenet", - "membersPanel": "Huoneen jäsenet ja hallintapyynnöt", - "membersWithCount": "Jäsenet ({{count}})", - "online": "verkossa", - "offline": "offline-tilassa", - "you": "(sinä)", - "hostBadge": "Isäntä", - "presenterBadge": "Esittäminen", - "present": "Esittää", - "presentTitle": "Nouse lavalle", - "presentHost": "Isäntä", - "searchHosts": "Hae isäntiä", - "noHostsFound": "Ei saatavilla olevia isäntiä.", - "presentProtocol": "Protokolla", - "stopPresenting": "Lopeta esittäminen", - "takeOver": "Ota haltuunsa", - "takeOverConfirmTitle": "Korvataanko nykyinen esitys?", - "takeOverDescription": "{{name}} esittää nyt. Hallinnan ottaminen irrottaa kaikki yhteydet kyseiseen vaiheeseen.", - "emptyStage": "Kukaan ei esitä. Nouse lavalle ja jaa sessio.", - "stageLoading": "Yhdistetään lavalle...", - "stageEnded": "Esitys päättyi", - "roomEnded": "Tämä kokous on päättynyt", - "presenterLabel": "{{name}} esittää", - "guestLink": "Vieraslinkki", - "guestLinkOn": "Vieraslinkki on käytössä. Kuka tahansa linkin omaava voi katsoa lavaa.", - "guestLinkOff": "Vierailijalinkki on pois päältä.", - "copyLink": "Kopioi linkki", - "rotateLink": "Kierrä linkkiä", - "disableLink": "Poista linkki käytöstä", - "rotateLinkConfirmTitle": "Kierretäänkö vieraslinkkiä?", - "rotateLinkDescription": "Nykyinen linkki lakkaa toimimasta välittömästi. Yhdistettyjen SSH-vieraiden yhteys katkaistaan.", - "disableLinkConfirmTitle": "Poistetaanko vieraslinkki käytöstä?", - "disableLinkDescription": "Anonyymien SSH-katsojien yhteys katkaistaan välittömästi ja nykyinen linkki lakkaa toimimasta.", - "linkCopied": "Linkki kopioitu", - "roles": "Roolit", - "users": "Käyttäjät", - "searchUsers": "Hae käyttäjiä", - "endConfirmTitle": "Lopetetaanko tämä kokous?", - "endDescription": "{{name}} päättyy kaikilta, eikä sitä voida avata uudelleen.", - "endPersistentDescription": "Aktiivinen esitys kohdassa {{name}} pysähtyy. Huone pysyy käytettävissä uudelleen.", - "deleteRoom": "Poista huone", - "deleteConfirmTitle": "Poistetaanko tämä huone?", - "deleteDescription": "{{name}} poistetaan pysyvästi kaikilta, eikä sitä voi perua.", - "invitedTo": "Sinut kutsuttiin \"{{name}} \" -palveluun", - "guest": { - "title": "Kokous", - "waiting": "Esiintyjää odotellessa...", - "linkInvalid": "Tämä vieraslinkki on virheellinen tai kokous on päättynyt" - }, - "reopenFromPanel": "Tämä kokousvälilehti vanheni. Avaa huone uudelleen Kokoukset-paneelista.", - "requestControl": "Pyydä hallintaa", - "cancelControlRequest": "Peruuta pyyntö", - "controlQueue": "Ohjauspyynnöt", - "noControlRequests": "Ei vireillä olevia pyyntöjä.", - "grantControlTo": "Anna hallinta käyttäjälle {{name}}", - "dismissControlRequest": "Hylkää käyttäjän {{name}}hallintapyyntö", - "releaseControl": "Vapautusohjaus", - "grantControl": "Anna hallinta", - "revokeControl": "Ota hallinta takaisin", - "controlRequestedBy": "{{name}} pyysi hallintaoikeutta", - "grant": "Myöntää", - "controlBadge": "Hallinnassa", - "youArePresenting": "Esität toisella välilehdellä. Lopeta esitys luovuttaaksesi esiintymispaikan.", - "created": "Huone luotu", - "invited": "Jäsenet kutsuttuina", - "roomTab": "Kokous" - }, "sessionSharing": { - "guestView": { - "loading": "Yhdistetään jaettuun istuntoon...", - "linkInvalid": "Tämä jakolinkki on virheellinen, vanhentunut tai se on peruutettu.", - "rateLimited": "Liikaa yrityksiä, yritä uudelleen pian", - "sessionEnded": "Tämä istunto on päättynyt", - "readOnlyBadge": "Vain katselu", - "ownerLabel": "Isännän omistaja" - }, - "modalTitle": "Jaa istunto", - "shareButton": "Jakaa", - "notReadyToShare": "Istunto ei ole vielä valmis jaettavaksi", - "modeTab": { - "link": "Linkki", - "user": "Käyttäjä" - }, - "linkModeDescription": "Kuka tahansa, jolla on tämä linkki, voi liittyä, tiliä ei tarvita.", - "userModeDescription": "Jaa tietyn käyttäjän kanssa, jolla on jo pääsy tähän isäntään. Jos heillä ei vielä ole pääsyä, jaa isäntä ensin heidän kanssaan tai käytä linkkiä. Jakamisen jälkeen istunto näkyy heidän Yhteydet-välilehdellään.", "permissionLevel": { - "label": "Käyttöoikeustaso", "readOnly": "Vain lukuoikeus", - "readOnlyDescription": "Voit katsoa istuntoa suorana, mutta et voi kirjoittaa tai olla vuorovaikutuksessa.", - "readWrite": "Lue ja kirjoita", - "readWriteDescription": "Voi kirjoittaa ja olla vuorovaikutuksessa istunnon kanssa aivan kuten omistaja." - }, - "expiryLabel": "Linkin vanheneminen", - "createLinkButton": "Luo linkki", - "createShareButton": "Jaa käyttäjän kanssa", - "searchUsersPlaceholder": "Hae käyttäjiä...", - "noUsersFound": "Ei käyttäjiä löytynyt", - "linkCreated": "Jaa linkki luotu", - "linkCopied": "Linkki kopioitu leikepöydälle", - "copyLink": "Kopioi linkki", - "shareCreated": "Istunto jaettu. Se näkyy heidän Yhteydet-välilehdellä.", - "shareFailed": "Jaon luominen epäonnistui", - "userLacksHostAccess": "Käyttäjällä ei ole vielä pääsyä tähän isäntään. Jaa isäntä ensin heidän kanssaan tai käytä linkkiä.", - "activeShares": "Aktiiviset jaot", - "noActiveShares": "Ei aktiivisia jakoja tällä istunnolla", - "revoke": "Peruuttaa", - "revokeConfirmTitle": "Perutaanko tämä jako?", - "revokeConfirmDescription": "Kuka tahansa tätä jakoa käyttävä menettää käyttöoikeuden välittömästi.", - "revoked": "Jako peruttu", - "revokeFailed": "Jakamisen peruuttaminen epäonnistui", - "joinCount": "{{count}} liity", - "joinCount_other": "{{count}} liitokset", - "expiresAt": "Vanhenee {{date}}", - "linkShareBadge": "Linkki", - "userShareBadge": "Käyttäjä: {{username}}", - "loadSharesFailed": "Aktiivisten jakojen lataaminen epäonnistui" + "readWrite": "Lue ja kirjoita" + } }, "terminal": { "connect": "Yhdistä isäntään", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Lähettää nimettömän päivittäisen määrän käyttäjistä, isännöistä ja ominaisuuksien käytöstä Termixin parantamiseksi. Henkilötietoja tai yhteystietoja ei koskaan sisällytetä mukaan.", "analyticsEnabledLockedDesc": "Tämä asetus on lukittu ENABLE_TELEMETRY-ympäristömuuttujalla, eikä sitä voi muuttaa täällä.", "updateAnalyticsFailed": "Analytiikka-asetuksen päivittäminen epäonnistui", - "sessionSharingGloballyEnabled": "Salli istunnon jakaminen", "auditForwardingTitle": "Lähetä SIEM:iin", "auditForwardingDesc": "Lähetä kopio jokaisesta auditointimerkinnästä ulkoiselle kerääjälle NDJSON-tiedostona. Paikalliset lokit pysyvät totuuden lähteenä.", "auditForwardingEnvNotice": "Ympäristömuuttuja määrittää myös edelleenlähetyksen; tähän tallennettu URL-osoite on etusijalla.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Haltijan tunnus (tallennettu)", "auditForwardingSaved": "Auditoinnin edelleenlähetysasetukset tallennettu", "auditForwardingSaveError": "Auditoinnin edelleenlähetysasetusten tallentaminen epäonnistui.", - "sessionSharingGloballyEnabledDesc": "Salli reaaliaikaisten pääte-, RDP-, VNC- ja Telnet-istuntojen jakaminen koko instanssin kesken. Poistamalla tämän asetuksen käytöstä ohitetaan kaikki isäntäkohtaisesti jakamiseen liittyvät asetukset.", - "updateSessionSharingFailed": "Istunnon jakamisasetuksen päivittäminen epäonnistui", "sessionTimeout": "Istunnon aikakatkaisu", "hours": "tuntia", "sessionTimeoutRange": "Vähintään 1 h · Enintään 720 h", diff --git a/src/ui/locales/translated/fr_FR.json b/src/ui/locales/translated/fr_FR.json index 9b136d7ec..a4111399f 100644 --- a/src/ui/locales/translated/fr_FR.json +++ b/src/ui/locales/translated/fr_FR.json @@ -667,7 +667,6 @@ "done_cli_link": "Ouvrir la documentation de l'interface de ligne de commande" }, "nav": { - "collab": "Réunions", "home": "Accueil", "terminal": "Terminal", "localTerminal": "Terminal local", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Pour que votre shell et tout ce qui y est exécuté restent actifs malgré les déconnexions et les redémarrages du navigateur, exécutez la commande dans tmux (tmux doit être installé sur l'hôte).", "enableSessionLogging": "Journalisation de session", "enableSessionLoggingDesc": "Enregistrer la sortie de la session terminal pour une consultation ultérieure", - "allowSessionSharing": "Autoriser le partage de session", - "allowSessionSharingDesc": "Autoriser le partage des sessions en direct sur cet hébergeur via un lien ou avec d'autres utilisateurs", "localEchoLabel": "Écho local", "localEchoDesc": "Prédiction locale des frappes au clavier imprimables en toute sécurité, même sur les connexions lentes. Les invites de mot de passe et les entrées de contrôle ne sont jamais affichées.", "localEchoDefault": "Utiliser les paramètres globaux", @@ -1571,137 +1568,11 @@ "join": "Rejoindre", "sharedSessionLabel": "{{hostName}} (partagé)" }, - "collab": { - "createRoom": "Nouvelle chambre", - "roomName": "Nom de la chambre", - "persistentRoom": "Chambre persistante", - "persistentRoomHint": "Les salles persistantes restent répertoriées après la fin de la réunion et peuvent être réutilisées.", - "noRooms": "Aucune salle disponible pour le moment. Créez-en une pour démarrer une réunion.", - "openRoom": "Ouvrir", - "endRoom": "Fin de la réunion", - "leaveRoom": "Partir", - "deleteMember": "Retirer", - "removeMember": "Supprimer {{name}}", - "removeMemberTitle": "Supprimer ce membre ?", - "removeMemberDescription": "{{name}} perdra immédiatement l'accès à cette salle. Tout utilisateur SSH actif sera déconnecté.", - "invite": "Inviter", - "inviteTitle": "Inviter des membres", - "members": "Membres", - "membersPanel": "Membres de la salle et demandes de contrôle", - "membersWithCount": "Membres ({{count}})", - "online": "en ligne", - "offline": "hors ligne", - "you": "(toi)", - "hostBadge": "Hôte", - "presenterBadge": "Présentation", - "present": "Présent", - "presentTitle": "Montez sur scène", - "presentHost": "Hôte", - "searchHosts": "Rechercher des hôtes", - "noHostsFound": "Aucun hôte disponible trouvé.", - "presentProtocol": "Protocole", - "stopPresenting": "Arrêtez de présenter", - "takeOver": "Reprendre", - "takeOverConfirmTitle": "Remplacer la présentation actuelle ?", - "takeOverDescription": "{{name}} est en train de présenter. Prendre le relais déconnectera tout le monde de cette scène.", - "emptyStage": "Personne ne présente. Montez sur scène pour animer une session.", - "stageLoading": "Se connecter à la scène...", - "stageEnded": "La présentation s'est terminée.", - "roomEnded": "Cette réunion est terminée.", - "presenterLabel": "{{name}} présente", - "guestLink": "Lien invité", - "guestLinkOn": "Le lien invité est activé. Toute personne disposant du lien peut assister à la représentation.", - "guestLinkOff": "Le lien invité est désactivé.", - "copyLink": "Copier le lien", - "rotateLink": "Lien de rotation", - "disableLink": "Désactiver le lien", - "rotateLinkConfirmTitle": "Faire pivoter le lien invité ?", - "rotateLinkDescription": "La connexion actuelle sera immédiatement interrompue. Les sessions SSH virtuelles seront déconnectées.", - "disableLinkConfirmTitle": "Désactiver le lien invité ?", - "disableLinkDescription": "Les utilisateurs anonymes utilisant SSH seront immédiatement déconnectés et le lien actuel cessera de fonctionner.", - "linkCopied": "Lien copié", - "roles": "Rôles", - "users": "Utilisateurs", - "searchUsers": "Utilisateurs de recherche", - "endConfirmTitle": "Mettre fin à cette réunion ?", - "endDescription": "{{name}} prendra fin pour tout le monde et ne pourra pas être rouvert.", - "endPersistentDescription": "La présentation en cours dans {{name}} va s'arrêter. La salle reste disponible pour une réutilisation.", - "deleteRoom": "Supprimer la salle", - "deleteConfirmTitle": "Supprimer cette pièce ?", - "deleteDescription": "{{name}} sera définitivement supprimé pour tout le monde et ne pourra pas être annulé.", - "invitedTo": "Vous avez été invité à \"{{name}}\"", - "guest": { - "title": "Réunion", - "waiting": "En attente du présentateur...", - "linkInvalid": "Ce lien invité est invalide ou la réunion est terminée." - }, - "reopenFromPanel": "Cette réunion a expiré. Veuillez rouvrir la salle depuis le panneau Réunions.", - "requestControl": "Contrôle des demandes", - "cancelControlRequest": "Demande d'annulation", - "controlQueue": "Demandes de contrôle", - "noControlRequests": "Aucune requête en attente.", - "grantControlTo": "Donnez le contrôle à {{name}}", - "dismissControlRequest": "Rejeter la demande de contrôle de {{name}}", - "releaseControl": "Contrôle de la libération", - "grantControl": "Donnez le contrôle", - "revokeControl": "Reprenez le contrôle", - "controlRequestedBy": "{{name}} a demandé le contrôle", - "grant": "Accorder", - "controlBadge": "En contrôle", - "youArePresenting": "Vous présentez dans un autre onglet. Arrêtez votre présentation pour laisser la parole à quelqu'un d'autre.", - "created": "Pièce créée", - "invited": "Les membres invités", - "roomTab": "Réunion" - }, "sessionSharing": { - "guestView": { - "loading": "Connexion à la session partagée...", - "linkInvalid": "Ce lien de partage est invalide, a expiré ou a été révoqué.", - "rateLimited": "Trop de tentatives, veuillez réessayer dans quelques instants.", - "sessionEnded": "Cette session est terminée.", - "readOnlyBadge": "Affichage uniquement", - "ownerLabel": "propriétaire de l'hôte" - }, - "modalTitle": "Session de partage", - "shareButton": "Partager", - "notReadyToShare": "La session n'est pas encore prête à être partagée.", - "modeTab": { - "link": "Lien", - "user": "Utilisateur" - }, - "linkModeDescription": "Toute personne disposant de ce lien peut s'inscrire, aucun compte n'est requis.", - "userModeDescription": "Partagez cette session avec un utilisateur spécifique qui y a déjà accès. S'il n'y a pas encore accès, partagez-lui d'abord l'hôte ou utilisez un lien. Une fois partagée, la session apparaîtra dans son onglet Connexions.", "permissionLevel": { - "label": "Niveau d'autorisation", "readOnly": "Lecture seule", - "readOnlyDescription": "Je peux suivre la session en direct, mais je ne peux ni écrire ni interagir.", - "readWrite": "Lecture-écriture", - "readWriteDescription": "Peut saisir du texte et interagir avec la session comme le propriétaire." - }, - "expiryLabel": "Expiration du lien", - "createLinkButton": "Créer un lien", - "createShareButton": "Partager avec l'utilisateur", - "searchUsersPlaceholder": "Rechercher des utilisateurs...", - "noUsersFound": "Aucun utilisateur trouvé", - "linkCreated": "Lien de partage créé", - "linkCopied": "Lien copié dans le presse-papiers", - "copyLink": "Copier le lien", - "shareCreated": "Session partagée. Elle apparaîtra dans leur onglet Connexions.", - "shareFailed": "Échec de la création du partage", - "userLacksHostAccess": "Cet utilisateur n'a pas encore accès à cet hôte. Veuillez d'abord lui partager l'hôte ou utiliser un lien.", - "activeShares": "Actions actives", - "noActiveShares": "Aucune action active pour cette session", - "revoke": "Révoquer", - "revokeConfirmTitle": "Révoquer cette action ?", - "revokeConfirmDescription": "Toute personne utilisant ce partage perdra immédiatement l'accès.", - "revoked": "Action révoquée", - "revokeFailed": "Échec de la révocation de l'action", - "joinCount": "{{count}} rejoindre", - "joinCount_other": "{{count}} rejoint", - "expiresAt": "Expire {{date}}", - "linkShareBadge": "Lien", - "userShareBadge": "Utilisateur : {{username}}", - "loadSharesFailed": "Échec du chargement des actions actives" + "readWrite": "Lecture-écriture" + } }, "terminal": { "connect": "Se connecter à l'hôte", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Envoie quotidiennement un décompte anonyme des utilisateurs, des hôtes et de l'utilisation des fonctionnalités afin d'améliorer Termix. Aucune donnée personnelle ni information de connexion n'est jamais incluse.", "analyticsEnabledLockedDesc": "Ce paramètre est verrouillé par la variable d'environnement ENABLE_TELEMETRY et ne peut pas être modifié ici.", "updateAnalyticsFailed": "Échec de la mise à jour des paramètres d'analyse", - "sessionSharingGloballyEnabled": "Autoriser le partage de session", "auditForwardingTitle": "Transmettre au SIEM", "auditForwardingDesc": "Transmettez une copie de chaque entrée d'audit à un collecteur externe au format NDJSON. Les journaux locaux restent la source de référence.", "auditForwardingEnvNotice": "Une variable d'environnement configure également la redirection ; l'URL enregistrée ici est prioritaire.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Jeton porteur (stocké)", "auditForwardingSaved": "Paramètres de transfert d'audit enregistrés", "auditForwardingSaveError": "Échec de l'enregistrement des paramètres de transfert d'audit", - "sessionSharingGloballyEnabledDesc": "Autorise le partage des sessions de terminal en direct, RDP, VNC et Telnet à l'échelle de l'instance. Remplace toute option de partage par hôte lorsqu'elle est désactivée.", - "updateSessionSharingFailed": "Échec de la mise à jour des paramètres de partage de session", "sessionTimeout": "Expiration de session", "hours": "heures", "sessionTimeoutRange": "Min 1 h · Max 720 h", diff --git a/src/ui/locales/translated/he_IL.json b/src/ui/locales/translated/he_IL.json index ee1e27410..5c16b1f21 100644 --- a/src/ui/locales/translated/he_IL.json +++ b/src/ui/locales/translated/he_IL.json @@ -667,7 +667,6 @@ "done_cli_link": "פתח את מסמכי ממשק שורת הפקודה (CLI)" }, "nav": { - "collab": "פגישות", "home": "בַּיִת", "terminal": "מָסוֹף", "localTerminal": "מסוף מקומי", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "שמרו על המעטפת שלכם וכל דבר שפועל בה בחיים גם לאחר ניתוקים והפעלות מחדש של הדפדפן על ידי הפעלתה בתוך tmux (tmux חייב להיות מותקן על המארח)", "enableSessionLogging": "רישום מושבים", "enableSessionLoggingDesc": "הקלט את פלט הפעלת הטרמינל לצורך סקירה מאוחרת יותר", - "allowSessionSharing": "אפשר שיתוף סשנים", - "allowSessionSharingDesc": "אפשר שיתוף של מפגשים חיים במארח זה באמצעות קישור או עם משתמשים אחרים", "localEchoLabel": "הד מקומי", "localEchoDesc": "חזה הקשות מקלדת להדפסה בטוחות באופן מקומי בחיבורים איטיים. בקשות סיסמה וקלט בקרה לעולם לא יוחזרו.", "localEchoDefault": "השתמש בהגדרה גלובלית", @@ -1571,137 +1568,11 @@ "join": "לְהִצְטַרֵף", "sharedSessionLabel": "{{hostName}} (שותף)" }, - "collab": { - "createRoom": "חדר חדש", - "roomName": "שם החדר", - "persistentRoom": "חדר קבוע", - "persistentRoomHint": "חדרים קבועים נשארים רשומים לאחר סיום הפגישה וניתן לעשות בהם שימוש חוזר.", - "noRooms": "אין עדיין חדרים. צור אחד כדי להתחיל פגישה.", - "openRoom": "לִפְתוֹחַ", - "endRoom": "סיום פגישה", - "leaveRoom": "לַעֲזוֹב", - "deleteMember": "לְהַסִיר", - "removeMember": "הסר {{name}}", - "removeMemberTitle": "להסיר את החבר הזה?", - "removeMemberDescription": "{{name}} יאבד גישה לחדר הזה באופן מיידי. כל צופה פעיל ב-SSH ינותק.", - "invite": "לְהַזמִין", - "inviteTitle": "הזמנת חברים", - "members": "חברים", - "membersPanel": "חברי חדר ובקשות בקרה", - "membersWithCount": "חברים ({{count}})", - "online": "באינטרנט", - "offline": "לא מקוון", - "you": "(אַתָה)", - "hostBadge": "מְאָרֵחַ", - "presenterBadge": "הצגה", - "present": "לְהַצִיג", - "presentTitle": "עלו על הבמה", - "presentHost": "מְאָרֵחַ", - "searchHosts": "חיפוש מארחים", - "noHostsFound": "לא נמצאו מארחים זמינים.", - "presentProtocol": "פּרוֹטוֹקוֹל", - "stopPresenting": "הפסקת ההצגה", - "takeOver": "השתלטות", - "takeOverConfirmTitle": "להחליף את המצגת הנוכחית?", - "takeOverDescription": "{{name}} מציג כעת. השתלטות תנתק את כולם מהבמה הזו.", - "emptyStage": "אף אחד לא מציג. עלו לבמה כדי לשתף סשן.", - "stageLoading": "מתחבר לבמה...", - "stageEnded": "המצגת הסתיימה", - "roomEnded": "פגישה זו הסתיימה", - "presenterLabel": "{{name}} מציג", - "guestLink": "קישור אורח", - "guestLinkOn": "קישור האורח פעיל. כל מי שיש לו קישור יכול לצפות בבמה.", - "guestLinkOff": "קישור האורח כבוי.", - "copyLink": "העתקת קישור", - "rotateLink": "סיבוב קישור", - "disableLink": "השבת קישור", - "rotateLinkConfirmTitle": "לסובב את קישור האורח?", - "rotateLinkDescription": "הקישור הנוכחי יפסיק לפעול באופן מיידי. אורחי SSH המחוברים ינותקו.", - "disableLinkConfirmTitle": "להשבית את קישור האורח?", - "disableLinkDescription": "צופי SSH אנונימיים ינותקו באופן מיידי והקישור הנוכחי יפסיק לפעול.", - "linkCopied": "הקישור הועתק", - "roles": "תפקידים", - "users": "משתמשים", - "searchUsers": "חיפוש משתמשים", - "endConfirmTitle": "לסיים את הפגישה הזו?", - "endDescription": "{{name}} יסתיים עבור כולם ולא ניתן יהיה לפתוח אותו מחדש.", - "endPersistentDescription": "המצגת הפעילה ב- {{name}} תיעצר. החדר יישאר זמין לשימוש חוזר.", - "deleteRoom": "מחיקת חדר", - "deleteConfirmTitle": "למחוק את החדר הזה?", - "deleteDescription": "{{name}} יימחק לצמיתות עבור כולם ולא ניתן יהיה לבטל אותו.", - "invitedTo": "הוזמנת ל-\"{{name}}\"", - "guest": { - "title": "פְּגִישָׁה", - "waiting": "מחכה למנחה...", - "linkInvalid": "קישור האורח הזה אינו תקין או שהפגישה הסתיימה" - }, - "reopenFromPanel": "פג תוקף כרטיסיית הפגישה הזו. פתח מחדש את החדר מחלונית הפגישות.", - "requestControl": "בקרת בקשות", - "cancelControlRequest": "ביטול בקשה", - "controlQueue": "בקשות בקרה", - "noControlRequests": "אין בקשות ממתינות.", - "grantControlTo": "תן שליטה ל- {{name}}", - "dismissControlRequest": "סגור את בקשת הבקרה של {{name}}", - "releaseControl": "בקרת שחרור", - "grantControl": "תן שליטה", - "revokeControl": "קח בחזרה את השליטה", - "controlRequestedBy": "{{name}} ביקש שליטה", - "grant": "מַעֲנָק", - "controlBadge": "בשליטה", - "youArePresenting": "אתה מציג בכרטיסייה אחרת. הפסק את ההצגה כדי להעביר את הבמה.", - "created": "החדר נוצר", - "invited": "חברים שהוזמנו", - "roomTab": "פְּגִישָׁה" - }, "sessionSharing": { - "guestView": { - "loading": "מתחבר לסשן משותף...", - "linkInvalid": "קישור השיתוף הזה אינו תקין, פג תוקף או בוטל", - "rateLimited": "יותר מדי ניסיונות, אנא נסה שוב בקרוב", - "sessionEnded": "מושב זה הסתיים", - "readOnlyBadge": "צפייה בלבד", - "ownerLabel": "בעל המארח" - }, - "modalTitle": "שיתוף סשן", - "shareButton": "לַחֲלוֹק", - "notReadyToShare": "הסשן עדיין לא מוכן לשיתוף", - "modeTab": { - "link": "לְקַשֵׁר", - "user": "מִשׁתַמֵשׁ" - }, - "linkModeDescription": "כל מי שיש לו קישור זה יכול להצטרף, אין צורך בחשבון.", - "userModeDescription": "שתף עם משתמש ספציפי שכבר יש לו גישה למארח זה. אם עדיין אין לו גישה, שתף איתו תחילה את המארח או השתמש בקישור במקום זאת. לאחר השיתוף, הסשן מופיע בכרטיסייה \"חיבורים\" שלו.", "permissionLevel": { - "label": "רמת הרשאה", "readOnly": "לקריאה בלבד", - "readOnlyDescription": "יכול לצפות בפגישה בשידור חי אך לא להקליד או לתקשר.", - "readWrite": "קריאה-כתיבה", - "readWriteDescription": "יכול להקליד ולקיים אינטראקציה עם הסשן בדיוק כמו הבעלים." - }, - "expiryLabel": "תוקף הקישור", - "createLinkButton": "צור קישור", - "createShareButton": "שתף עם המשתמש", - "searchUsersPlaceholder": "חיפוש משתמשים...", - "noUsersFound": "לא נמצאו משתמשים", - "linkCreated": "קישור השיתוף נוצר", - "linkCopied": "קישור שהועתק ללוח", - "copyLink": "העתקת קישור", - "shareCreated": "הסשן שותף. זה יופיע בלשונית החיבורים שלהם.", - "shareFailed": "יצירת השיתוף נכשלה", - "userLacksHostAccess": "למשתמש הזה עדיין אין גישה למארח הזה. שתף את המארח איתו תחילה, או השתמש בקישור במקום זאת.", - "activeShares": "מניות פעילות", - "noActiveShares": "אין שיתופים פעילים עבור סשן זה", - "revoke": "לְבַטֵל", - "revokeConfirmTitle": "לבטל את השיתוף הזה?", - "revokeConfirmDescription": "כל מי שישתמש בשיתוף הזה יאבד גישה באופן מיידי.", - "revoked": "השיתוף בוטל", - "revokeFailed": "ביטול השיתוף נכשל", - "joinCount": "{{count}} join", - "joinCount_other": "{{count}} מצטרף", - "expiresAt": "פג תוקף {{date}}", - "linkShareBadge": "לְקַשֵׁר", - "userShareBadge": "משתמש: {{username}}", - "loadSharesFailed": "נכשלה טעינת השיתופים הפעילים" + "readWrite": "קריאה-כתיבה" + } }, "terminal": { "connect": "התחברות למארח", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "שולח ספירה יומית אנונימית של משתמשים, מארחים ושימוש בתכונות כדי לסייע בשיפור Termix. לא כלולים נתונים אישיים או פרטי חיבור.", "analyticsEnabledLockedDesc": "הגדרה זו נעולה על ידי משתנה הסביבה ENABLE_TELEMETRY ולא ניתן לשנות אותה כאן.", "updateAnalyticsFailed": "נכשל עדכון הגדרת הניתוח", - "sessionSharingGloballyEnabled": "אפשר שיתוף סשנים", "auditForwardingTitle": "העברה ל-SIEM", "auditForwardingDesc": "שלח עותק של כל ערך ביקורת לאספן חיצוני כ-NDJSON. יומני רישום מקומיים נשארים מקור האמת.", "auditForwardingEnvNotice": "משתנה סביבה גם מגדיר העברה; כתובת ה-URL שנשמרת כאן מקבלת עדיפות.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "אסימון נושא (מאוחסן)", "auditForwardingSaved": "הגדרות העברת ביקורת נשמרו", "auditForwardingSaveError": "נכשלה שמירת הגדרות העברת הביקורת", - "sessionSharingGloballyEnabledDesc": "מאפשר שיתוף של הפעלות מסוף חי, RDP, VNC ו-Telnet בכל המופעים. מבטל כל אפשרות שיתוף לפי מארח כאשר היא מושבתת.", - "updateSessionSharingFailed": "נכשל עדכון הגדרת שיתוף הסשן", "sessionTimeout": "פסק זמן של סשן", "hours": "שעות", "sessionTimeoutRange": "מינימום שעה · מקסימום 720 שעות", diff --git a/src/ui/locales/translated/hi_IN.json b/src/ui/locales/translated/hi_IN.json index 690ef8dae..aa0446468 100644 --- a/src/ui/locales/translated/hi_IN.json +++ b/src/ui/locales/translated/hi_IN.json @@ -667,7 +667,6 @@ "done_cli_link": "CLI दस्तावेज़ खोलें" }, "nav": { - "collab": "बैठक", "home": "होम", "terminal": "टर्मिनल", "localTerminal": "स्थानीय टर्मिनल", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "अपने शेल और उसमें चल रहे किसी भी चीज़ को डिस्कनेक्ट होने और ब्राउज़र के रीस्टार्ट होने पर भी चालू रखने के लिए, tmux के अंदर चलाएँ (होस्ट पर tmux इंस्टॉल होना चाहिए)।", "enableSessionLogging": "सत्र लॉगिंग", "enableSessionLoggingDesc": "बाद में समीक्षा के लिए टर्मिनल सत्र का आउटपुट रिकॉर्ड करें", - "allowSessionSharing": "सेशन शेयरिंग की अनुमति दें", - "allowSessionSharingDesc": "इस होस्ट पर होने वाले लाइव सेशन को लिंक के माध्यम से या अन्य उपयोगकर्ताओं के साथ साझा किया जा सकता है।", "localEchoLabel": "स्थानीय प्रतिध्वनि", "localEchoDesc": "धीमे कनेक्शन पर स्थानीय रूप से सुरक्षित और प्रिंट करने योग्य कीस्ट्रोक्स का अनुमान लगाएं। पासवर्ड प्रॉम्प्ट और नियंत्रण इनपुट कभी भी प्रदर्शित नहीं होते हैं।", "localEchoDefault": "वैश्विक सेटिंग का उपयोग करें", @@ -1571,137 +1568,11 @@ "join": "जोड़ना", "sharedSessionLabel": "{{hostName}} (साझा)" }, - "collab": { - "createRoom": "नया कमरा", - "roomName": "कमरे का नाम", - "persistentRoom": "लगातार कमरा", - "persistentRoomHint": "स्थायी कमरे मीटिंग समाप्त होने के बाद भी सूची में बने रहते हैं और उनका पुनः उपयोग किया जा सकता है।", - "noRooms": "अभी कोई कमरा उपलब्ध नहीं है। मीटिंग शुरू करने के लिए एक कमरा बनाएं।", - "openRoom": "खुला", - "endRoom": "बैठक समाप्त", - "leaveRoom": "छुट्टी", - "deleteMember": "निकालना", - "removeMember": "{{name}} हटाएँ", - "removeMemberTitle": "क्या इस सदस्य को हटा दें?", - "removeMemberDescription": "{{name}} को इस कमरे तक पहुँच तुरंत खोनी पड़ेगी। कोई भी सक्रिय SSH व्यूअर डिस्कनेक्ट हो जाएगा।", - "invite": "आमंत्रित करना", - "inviteTitle": "सदस्यों को आमंत्रित करो", - "members": "सदस्यों", - "membersPanel": "कमरे के सदस्य और नियंत्रण अनुरोध", - "membersWithCount": "सदस्य ({{count}})", - "online": "ऑनलाइन", - "offline": "ऑफलाइन", - "you": "(आप)", - "hostBadge": "मेज़बान", - "presenterBadge": "प्रस्तुत करते हुए", - "present": "उपस्थित", - "presentTitle": "मंच पर आइए", - "presentHost": "मेज़बान", - "searchHosts": "होस्ट खोजें", - "noHostsFound": "कोई उपलब्ध होस्ट नहीं मिला।", - "presentProtocol": "शिष्टाचार", - "stopPresenting": "प्रस्तुति देना बंद करो", - "takeOver": "कब्जा", - "takeOverConfirmTitle": "क्या वर्तमान प्रस्तुति को बदलना है?", - "takeOverDescription": "{{name}} अभी प्रस्तुति दे रहा है। कार्यभार संभालने से सभी लोग उस मंच से अलग हो जाएंगे।", - "emptyStage": "आज कोई प्रस्तुति नहीं दे रहा है। आप मंच पर आकर अपना सत्र साझा करें।", - "stageLoading": "मंच से जुड़ना...", - "stageEnded": "प्रस्तुति समाप्त हुई", - "roomEnded": "यह बैठक समाप्त हो गई है", - "presenterLabel": "{{name}} प्रस्तुत कर रहा है", - "guestLink": "अतिथि लिंक", - "guestLinkOn": "अतिथि लिंक चालू है। लिंक के माध्यम से कोई भी व्यक्ति मंच देख सकता है।", - "guestLinkOff": "अतिथि लिंक बंद है।", - "copyLink": "लिंक की प्रतिलिपि करें", - "rotateLink": "लिंक घुमाएँ", - "disableLink": "लिंक अक्षम करें", - "rotateLinkConfirmTitle": "अतिथि लिंक को घुमाएँ?", - "rotateLinkDescription": "वर्तमान लिंक तुरंत काम करना बंद कर देगा। कनेक्टेड एसएसएच गेस्ट डिस्कनेक्ट हो जाएंगे।", - "disableLinkConfirmTitle": "अतिथि लिंक को निष्क्रिय करें?", - "disableLinkDescription": "अनाम एसएसएच व्यूअर्स तुरंत डिस्कनेक्ट हो जाएंगे और वर्तमान लिंक काम करना बंद कर देगा।", - "linkCopied": "लिंक कॉपी हो गया", - "roles": "भूमिकाएँ", - "users": "उपयोगकर्ताओं", - "searchUsers": "उपयोगकर्ताओं को खोजें", - "endConfirmTitle": "क्या इस बैठक को यहीं समाप्त करें?", - "endDescription": "{{name}} सभी के लिए समाप्त हो जाएगा और इसे दोबारा नहीं खोला जा सकता है।", - "endPersistentDescription": "{{name}} में चल रही प्रस्तुति बंद हो जाएगी। कमरा पुनः उपयोग के लिए उपलब्ध रहेगा।", - "deleteRoom": "कमरा हटाएं", - "deleteConfirmTitle": "क्या इस कमरे को हटा दें?", - "deleteDescription": "{{name}} को सभी के लिए स्थायी रूप से हटा दिया जाएगा और इसे पूर्ववत नहीं किया जा सकता है।", - "invitedTo": "आपको \"{{name}} \" में आमंत्रित किया गया था", - "guest": { - "title": "बैठक", - "waiting": "प्रस्तुतकर्ता का इंतजार है...", - "linkInvalid": "यह अतिथि लिंक अमान्य है या मीटिंग समाप्त हो चुकी है" - }, - "reopenFromPanel": "इस मीटिंग टैब की समय सीमा समाप्त हो गई है। कृपया मीटिंग पैनल से कमरे को पुनः खोलें।", - "requestControl": "अनुरोध नियंत्रण", - "cancelControlRequest": "अनुरोध को रद्द करें", - "controlQueue": "नियंत्रण अनुरोध", - "noControlRequests": "कोई लंबित अनुरोध नहीं।", - "grantControlTo": "{{name}} को नियंत्रण दें", - "dismissControlRequest": "{{name}}के नियंत्रण अनुरोध को खारिज करें", - "releaseControl": "नियंत्रण छोड़ें", - "grantControl": "नियंत्रण सौंप दें", - "revokeControl": "नियंत्रण वापस अपने हाथ में लें।", - "controlRequestedBy": "{{name}} ने नियंत्रण मांगा", - "grant": "अनुदान", - "controlBadge": "नियंत्रण में", - "youArePresenting": "आप किसी दूसरे टैब में प्रेजेंटेशन दे रहे हैं। कृपया प्रेजेंटेशन देना बंद करें और स्टेज किसी और को सौंप दें।", - "created": "कमरा बनाया गया", - "invited": "सदस्यों को आमंत्रित किया गया", - "roomTab": "बैठक" - }, "sessionSharing": { - "guestView": { - "loading": "साझा सत्र से जुड़ रहा है...", - "linkInvalid": "यह शेयर लिंक अमान्य है, इसकी समय सीमा समाप्त हो चुकी है, या इसे रद्द कर दिया गया है।", - "rateLimited": "बहुत अधिक प्रयास किए गए, कृपया कुछ देर बाद पुनः प्रयास करें।", - "sessionEnded": "यह सत्र समाप्त हो गया है।", - "readOnlyBadge": "केवल देखें", - "ownerLabel": "मेज़बान स्वामी" - }, - "modalTitle": "सत्र साझा करें", - "shareButton": "शेयर करना", - "notReadyToShare": "सेशन अभी शेयर करने के लिए तैयार नहीं है", - "modeTab": { - "link": "जोड़ना", - "user": "उपयोगकर्ता" - }, - "linkModeDescription": "इस लिंक के जरिए कोई भी जुड़ सकता है, इसके लिए किसी खाते की आवश्यकता नहीं है।", - "userModeDescription": "इसे उस विशिष्ट उपयोगकर्ता के साथ साझा करें जिसके पास पहले से ही इस होस्ट का एक्सेस है। यदि उनके पास अभी एक्सेस नहीं है, तो पहले उनके साथ होस्ट साझा करें या इसके बजाय एक लिंक का उपयोग करें। साझा करने के बाद, सेशन उनके कनेक्शन टैब में दिखाई देगा।", "permissionLevel": { - "label": "अनुमति स्तर", "readOnly": "केवल पढ़ने के लिए", - "readOnlyDescription": "आप सेशन को लाइव देख सकते हैं लेकिन टाइप या इंटरैक्ट नहीं कर सकते।", - "readWrite": "पढ़ना-लिखना", - "readWriteDescription": "मालिक की तरह ही सेशन में टाइप कर सकता है और इंटरैक्ट कर सकता है।" - }, - "expiryLabel": "लिंक की समय सीमा समाप्त", - "createLinkButton": "लिंक बनाएं", - "createShareButton": "उपयोगकर्ता के साथ साझा करें", - "searchUsersPlaceholder": "उपयोगकर्ताओं को खोजें...", - "noUsersFound": "कोई उपयोगकर्ता नहीं मिला", - "linkCreated": "शेयर लिंक बनाया गया", - "linkCopied": "लिंक क्लिपबोर्ड पर कॉपी हो गया है", - "copyLink": "लिंक की प्रतिलिपि करें", - "shareCreated": "सेशन साझा कर दिया गया है। यह उनके कनेक्शन टैब में दिखाई देगा।", - "shareFailed": "शेयर बनाने में विफल", - "userLacksHostAccess": "उस उपयोगकर्ता के पास अभी इस होस्ट तक पहुंच नहीं है। कृपया पहले उनके साथ होस्ट साझा करें, या इसके बजाय एक लिंक का उपयोग करें।", - "activeShares": "सक्रिय शेयर", - "noActiveShares": "इस सत्र के लिए कोई सक्रिय शेयर नहीं हैं", - "revoke": "रद्द करना", - "revokeConfirmTitle": "क्या इस शेयर को रद्द कर दें?", - "revokeConfirmDescription": "इस शेयर का उपयोग करने वाले किसी भी व्यक्ति की पहुंच तुरंत समाप्त हो जाएगी।", - "revoked": "शेयर रद्द कर दिया गया", - "revokeFailed": "शेयर रद्द करने में विफल", - "joinCount": "{{count}} जुड़ें", - "joinCount_other": "{{count}} जुड़ता है", - "expiresAt": "समाप्ति तिथि {{date}}", - "linkShareBadge": "जोड़ना", - "userShareBadge": "उपयोगकर्ता: {{username}}", - "loadSharesFailed": "सक्रिय शेयर लोड करने में विफल" + "readWrite": "पढ़ना-लिखना" + } }, "terminal": { "connect": "होस्ट से कनेक्ट करें", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "यह Termix को बेहतर बनाने में मदद करने के लिए उपयोगकर्ताओं, होस्टों और फ़ीचर उपयोग की दैनिक गुमनाम गणना भेजता है। इसमें कभी भी कोई व्यक्तिगत डेटा या कनेक्शन विवरण शामिल नहीं किया जाता है।", "analyticsEnabledLockedDesc": "यह सेटिंग ENABLE_TELEMETRY पर्यावरण चर द्वारा लॉक की गई है और इसे यहां बदला नहीं जा सकता है।", "updateAnalyticsFailed": "एनालिटिक्स सेटिंग को अपडेट करने में विफल", - "sessionSharingGloballyEnabled": "सेशन शेयरिंग की अनुमति दें", "auditForwardingTitle": "SIEM को अग्रेषित करें", "auditForwardingDesc": "प्रत्येक ऑडिट प्रविष्टि की एक प्रति NDJSON प्रारूप में बाहरी संग्राहक को भेजें। स्थानीय लॉग ही प्रामाणिक स्रोत बने रहेंगे।", "auditForwardingEnvNotice": "एक पर्यावरण चर फ़ॉरवर्डिंग को भी कॉन्फ़िगर करता है; यहाँ सहेजा गया URL प्राथमिकता लेता है।", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "बियरर टोकन (संग्रहीत)", "auditForwardingSaved": "ऑडिट अग्रेषण सेटिंग्स सहेजी गईं", "auditForwardingSaveError": "ऑडिट फ़ॉरवर्डिंग सेटिंग्स को सहेजने में विफलता", - "sessionSharingGloballyEnabledDesc": "लाइव टर्मिनल, RDP, VNC और Telnet सेशन को पूरे इंस्टेंस में साझा करने की अनुमति दें। अक्षम होने पर यह प्रति-होस्ट साझाकरण टॉगल को ओवरराइड कर देता है।", - "updateSessionSharingFailed": "सेशन शेयरिंग सेटिंग को अपडेट करने में विफलता", "sessionTimeout": "सत्र टाइमआउट", "hours": "घंटे", "sessionTimeoutRange": "न्यूनतम 1 घंटा · अधिकतम 720 घंटे", diff --git a/src/ui/locales/translated/hu_HU.json b/src/ui/locales/translated/hu_HU.json index 94068a63c..62b7b5066 100644 --- a/src/ui/locales/translated/hu_HU.json +++ b/src/ui/locales/translated/hu_HU.json @@ -667,7 +667,6 @@ "done_cli_link": "Nyissa meg a CLI dokumentációját" }, "nav": { - "collab": "Találkozók", "home": "Otthon", "terminal": "Terminál", "localTerminal": "Helyi terminál", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Tartsd életben a shell-edet és a benne futó dolgokat a lecsatlakozások és a böngésző újraindítása során a tmux-on belüli futtatással (a tmux-nak telepítve kell lennie a gazdagépen)", "enableSessionLogging": "Munkamenet-naplózás", "enableSessionLoggingDesc": "Terminál munkamenet kimenetének rögzítése későbbi áttekintés céljából", - "allowSessionSharing": "Munkamenet-megosztás engedélyezése", - "allowSessionSharingDesc": "Engedélyezi az élő munkamenetek megosztását ezen a gazdagépen linken vagy más felhasználókkal", "localEchoLabel": "Helyi visszhang", "localEchoDesc": "Lassú kapcsolatok esetén is helyben előrejelzi a biztonságosan nyomtatható billentyűleütéseket. A jelszókérések és a vezérlőbevitel soha nem jelenik meg.", "localEchoDefault": "Globális beállítás használata", @@ -1571,137 +1568,11 @@ "join": "Csatlakozik", "sharedSessionLabel": "{{hostName}} (megosztva)" }, - "collab": { - "createRoom": "Új szoba", - "roomName": "Szoba neve", - "persistentRoom": "Állandó szoba", - "persistentRoomHint": "Az állandó szobák a megbeszélés befejezése után is listázva maradnak, és újra felhasználhatók.", - "noRooms": "Még nincsenek szobák. Hozz létre egyet a megbeszélés megkezdéséhez.", - "openRoom": "Nyitott", - "endRoom": "Megbeszélés vége", - "leaveRoom": "Szabadság", - "deleteMember": "Eltávolítás", - "removeMember": "Eltávolítás {{name}}", - "removeMemberTitle": "Eltávolítja ezt a tagot?", - "removeMemberDescription": "{{name}} azonnal elveszíti a hozzáférést ehhez a szobához. Minden aktív SSH-megjelenítő lekapcsolódik.", - "invite": "Meghív", - "inviteTitle": "Tagok meghívása", - "members": "Tagok", - "membersPanel": "Szoba tagjai és irányítási kérelmek", - "membersWithCount": "Tagok ({{count}})", - "online": "online", - "offline": "offline", - "you": "(te)", - "hostBadge": "Házigazda", - "presenterBadge": "Bemutatja", - "present": "Jelenlegi", - "presentTitle": "Lépj színpadra", - "presentHost": "Házigazda", - "searchHosts": "Keresés hosztok", - "noHostsFound": "Nem található elérhető host.", - "presentProtocol": "Jegyzőkönyv", - "stopPresenting": "Prezentáció leállítása", - "takeOver": "Átvétel", - "takeOverConfirmTitle": "Lecseréli a jelenlegi prezentációt?", - "takeOverDescription": "{{name}} most tart előadást. Az irányítás átvétele mindenkit lekapcsolna arról a színpadról.", - "emptyStage": "Senki sem tart előadást. Lépjen színpadra, és ossza meg velünk az előadást.", - "stageLoading": "Csatlakozás a színpadhoz...", - "stageEnded": "A prezentáció véget ért", - "roomEnded": "Ez a találkozó véget ért", - "presenterLabel": "{{name}} bemutatja", - "guestLink": "Vendéglink", - "guestLinkOn": "Vendéglink be van kapcsolva. Bárki, aki rendelkezik a linkkel, nézheti a színpadot.", - "guestLinkOff": "A vendéglink ki van kapcsolva.", - "copyLink": "Link másolása", - "rotateLink": "Link elforgatása", - "disableLink": "Link letiltása", - "rotateLinkConfirmTitle": "Vendéglink elforgatása?", - "rotateLinkDescription": "A jelenlegi kapcsolat azonnal leáll. A csatlakoztatott SSH-vendégek le lesznek választva.", - "disableLinkConfirmTitle": "Letiltja a vendéglinket?", - "disableLinkDescription": "A névtelen SSH-nézők azonnal le lesznek választva, és a jelenlegi link nem fog működni.", - "linkCopied": "Link másolva", - "roles": "Szerepkörök", - "users": "Felhasználók", - "searchUsers": "Felhasználók keresése", - "endConfirmTitle": "Befejezi ezt a megbeszélést?", - "endDescription": "{{name}} mindenki számára megszűnik, és nem lehet újra megnyitni.", - "endPersistentDescription": "A {{name}} teremben futó aktív prezentáció leáll. A szoba továbbra is elérhető marad újrafelhasználásra.", - "deleteRoom": "Szoba törlése", - "deleteConfirmTitle": "Törli ezt a szobát?", - "deleteDescription": "A {{name}} véglegesen törlésre kerül mindenki számára, és a művelet nem vonható vissza.", - "invitedTo": "Meghívtak a következőre: \"{{name}}\"", - "guest": { - "title": "Találkozó", - "waiting": "Várjuk az előadót...", - "linkInvalid": "Ez a vendéglink érvénytelen, vagy a megbeszélés véget ért." - }, - "reopenFromPanel": "Ez a megbeszélésfül lejárt. Nyissa meg újra a szobát a Megbeszélések panelről.", - "requestControl": "Kérelemvezérlés", - "cancelControlRequest": "Kérés visszavonása", - "controlQueue": "Ellenőrzési kérelmek", - "noControlRequests": "Nincsenek függőben lévő kérések.", - "grantControlTo": "Átadni az irányítást {{name}}-nak", - "dismissControlRequest": "Elutasítom {{name}}vezérlési kérelmét", - "releaseControl": "Kioldásvezérlés", - "grantControl": "Átadni az irányítást", - "revokeControl": "Vedd vissza az irányítást", - "controlRequestedBy": "{{name}} kérte az irányítást", - "grant": "Támogatás", - "controlBadge": "Irányítás alatt", - "youArePresenting": "Egy másik lapon tartasz előadást. Állítsa le az előadást, hogy átadhassa a helyet.", - "created": "Szoba létrehozva", - "invited": "Meghívott tagok", - "roomTab": "Találkozó" - }, "sessionSharing": { - "guestView": { - "loading": "Kapcsolódás a megosztott munkamenethez...", - "linkInvalid": "Ez a megosztási link érvénytelen, lejárt vagy visszavonták.", - "rateLimited": "Túl sok próbálkozás, kérjük, próbáld újra később", - "sessionEnded": "Ez a munkamenet véget ért", - "readOnlyBadge": "Csak megtekintés", - "ownerLabel": "Házigazda tulajdonos" - }, - "modalTitle": "Megosztási munkamenet", - "shareButton": "Részesedés", - "notReadyToShare": "A munkamenet még nem megosztható", - "modeTab": { - "link": "Link", - "user": "Felhasználó" - }, - "linkModeDescription": "Bárki csatlakozhat a link birtokában, nincs szükség regisztrációra.", - "userModeDescription": "Megosztás egy adott felhasználóval, aki már rendelkezik hozzáféréssel ehhez a gazdagéphez. Ha még nincs hozzáférésük, először oszd meg vele a gazdagépet, vagy használj hivatkozást. A megosztás után a munkamenet megjelenik a Kapcsolatok lapon.", "permissionLevel": { - "label": "Jogosultsági szint", "readOnly": "Csak olvasható", - "readOnlyDescription": "Élőben nézhetem az előadást, de nem gépelhetek és nem léphetek interakcióba.", - "readWrite": "Olvasható-írható", - "readWriteDescription": "Pont úgy tud gépelni és kommunikálni a munkamenettel, mint a tulajdonos." - }, - "expiryLabel": "Link lejárata", - "createLinkButton": "Link létrehozása", - "createShareButton": "Megosztás a felhasználóval", - "searchUsersPlaceholder": "Felhasználók keresése...", - "noUsersFound": "Nem található felhasználó", - "linkCreated": "Megosztási link létrehozva", - "linkCopied": "Link a vágólapra másolva", - "copyLink": "Link másolása", - "shareCreated": "Munkamenet megosztva. Megjelenik a Kapcsolatok fülön.", - "shareFailed": "Nem sikerült létrehozni a megosztást", - "userLacksHostAccess": "A felhasználónak még nincs hozzáférése ehhez a gazdagéphez. Először oszd meg vele a gazdagépet, vagy használj linket.", - "activeShares": "Aktív megosztások", - "noActiveShares": "Nincsenek aktív megosztások ebben a munkamenetben", - "revoke": "Visszavonás", - "revokeConfirmTitle": "Visszavonja ezt a megosztást?", - "revokeConfirmDescription": "Bárki, aki ezt a megosztást használja, azonnal elveszíti a hozzáférését.", - "revoked": "Megosztás visszavonva", - "revokeFailed": "Nem sikerült visszavonni a megosztást", - "joinCount": "{{count}} csatlakozás", - "joinCount_other": "{{count}} csatlakozások", - "expiresAt": "Lejár {{date}}", - "linkShareBadge": "Link", - "userShareBadge": "Felhasználó: {{username}}", - "loadSharesFailed": "Nem sikerült betölteni az aktív megosztásokat" + "readWrite": "Olvasható-írható" + } }, "terminal": { "connect": "Csatlakozás a gazdagéphez", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Naponta névtelenül küldi a felhasználók, a hosztok és a funkciók használatának számát a Termix fejlesztése érdekében. Személyes adatokat vagy kapcsolati adatokat nem tartalmaz.", "analyticsEnabledLockedDesc": "Ezt a beállítást az ENABLE_TELEMETRY környezeti változó zárolja, és itt nem módosítható.", "updateAnalyticsFailed": "Nem sikerült frissíteni az analitikai beállításokat", - "sessionSharingGloballyEnabled": "Munkamenet-megosztás engedélyezése", "auditForwardingTitle": "Tovább a SIEM-hez", "auditForwardingDesc": "Minden auditbejegyzés másolatát küldd el egy külső gyűjtőnek NDJSON formátumban. A helyi naplók továbbra is az igazság forrásai.", "auditForwardingEnvNotice": "Egy környezeti változó is konfigurálja a továbbítás funkciót; az itt mentett URL élvez elsőbbséget.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Tulajdonosi token (tárolt)", "auditForwardingSaved": "Audittovábbítási beállítások mentve", "auditForwardingSaveError": "Nem sikerült menteni az audittovábbítási beállításokat", - "sessionSharingGloballyEnabledDesc": "Engedélyezi az élő terminál-, RDP-, VNC- és Telnet-munkamenetek példányonkénti megosztását. Letiltás esetén felülírja az összes gazdagépenkénti megosztási kapcsolót.", - "updateSessionSharingFailed": "Nem sikerült frissíteni a munkamenet-megosztási beállítást", "sessionTimeout": "Munkamenet időkorlátja", "hours": "órák", "sessionTimeoutRange": "Minimum 1 óra · Max. 720 óra", diff --git a/src/ui/locales/translated/id_ID.json b/src/ui/locales/translated/id_ID.json index f0c224def..6b8f847a4 100644 --- a/src/ui/locales/translated/id_ID.json +++ b/src/ui/locales/translated/id_ID.json @@ -667,7 +667,6 @@ "done_cli_link": "Buka dokumentasi CLI" }, "nav": { - "collab": "Pertemuan", "home": "Rumah", "terminal": "Terminal", "localTerminal": "Terminal Lokal", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Jaga agar shell Anda dan semua yang berjalan di dalamnya tetap aktif meskipun terjadi pemutusan koneksi dan restart browser dengan menjalankan perintah di dalam tmux (tmux harus diinstal pada host).", "enableSessionLogging": "Pencatatan Sesi", "enableSessionLoggingDesc": "Rekam output sesi terminal untuk ditinjau nanti.", - "allowSessionSharing": "Izinkan Berbagi Sesi", - "allowSessionSharingDesc": "Izinkan sesi langsung di host ini dibagikan melalui tautan atau dengan pengguna lain.", "localEchoLabel": "Gema Lokal", "localEchoDesc": "Memprediksi penekanan tombol yang aman dan dapat dicetak secara lokal pada koneksi lambat. Permintaan kata sandi dan input kontrol tidak pernah diulang.", "localEchoDefault": "Gunakan pengaturan global", @@ -1571,137 +1568,11 @@ "join": "Bergabung", "sharedSessionLabel": "{{hostName}} (dibagikan)" }, - "collab": { - "createRoom": "Kamar baru", - "roomName": "Nama ruangan", - "persistentRoom": "Ruang permanen", - "persistentRoomHint": "Ruangan yang bersifat permanen akan tetap terdaftar setelah rapat berakhir dan dapat digunakan kembali.", - "noRooms": "Belum ada ruang rapat. Buat ruang rapat untuk memulai pertemuan.", - "openRoom": "Membuka", - "endRoom": "Mengakhiri pertemuan", - "leaveRoom": "Meninggalkan", - "deleteMember": "Menghapus", - "removeMember": "Hapus {{name}}", - "removeMemberTitle": "Hapus anggota ini?", - "removeMemberDescription": "{{name}} akan kehilangan akses ke ruangan ini segera. Setiap penonton SSH yang aktif akan terputus.", - "invite": "Mengundang", - "inviteTitle": "Undang anggota", - "members": "Anggota", - "membersPanel": "Anggota ruangan dan permintaan kontrol", - "membersWithCount": "Anggota ({{count}})", - "online": "on line", - "offline": "offline", - "you": "(Anda)", - "hostBadge": "Tuan rumah", - "presenterBadge": "Mempersembahkan", - "present": "Hadiah", - "presentTitle": "Naik ke panggung", - "presentHost": "Tuan rumah", - "searchHosts": "Cari host", - "noHostsFound": "Tidak ditemukan host yang tersedia.", - "presentProtocol": "Protokol", - "stopPresenting": "Berhenti melakukan presentasi", - "takeOver": "Mengambil alih", - "takeOverConfirmTitle": "Mengganti presentasi yang ada?", - "takeOverDescription": "{{name}} sedang melakukan presentasi sekarang. Mengambil alih akan memutuskan koneksi semua orang dari panggung tersebut.", - "emptyStage": "Tidak ada yang sedang presentasi. Silakan naik ke panggung untuk berbagi sesi.", - "stageLoading": "Terhubung ke panggung...", - "stageEnded": "Presentasi berakhir", - "roomEnded": "Pertemuan ini telah berakhir.", - "presenterLabel": "{{name}} sedang menampilkan", - "guestLink": "Tautan tamu", - "guestLinkOn": "Tautan tamu sudah aktif. Siapa pun yang memiliki tautan tersebut dapat menonton pertunjukan di panggung.", - "guestLinkOff": "Tautan tamu dinonaktifkan.", - "copyLink": "Salin tautan", - "rotateLink": "Putar tautan", - "disableLink": "Nonaktifkan tautan", - "rotateLinkConfirmTitle": "Putar tautan tamu?", - "rotateLinkDescription": "Koneksi saat ini akan segera berhenti berfungsi. Tamu SSH yang terhubung akan terputus.", - "disableLinkConfirmTitle": "Nonaktifkan tautan tamu?", - "disableLinkDescription": "Pengguna anonim yang mengakses melalui SSH akan langsung terputus dan tautan saat ini akan berhenti berfungsi.", - "linkCopied": "Tautan disalin", - "roles": "Peran", - "users": "Pengguna", - "searchUsers": "Cari pengguna", - "endConfirmTitle": "Apakah pertemuan ini bisa diakhiri?", - "endDescription": "{{name}} akan berakhir untuk semua orang dan tidak dapat dibuka kembali.", - "endPersistentDescription": "Presentasi aktif di {{name}} akan berhenti. Ruangan tetap tersedia untuk digunakan kembali.", - "deleteRoom": "Hapus ruangan", - "deleteConfirmTitle": "Hapus ruangan ini?", - "deleteDescription": "{{name}} akan dihapus secara permanen untuk semua orang dan tidak dapat dibatalkan.", - "invitedTo": "Anda diundang ke \"{{name}}\"", - "guest": { - "title": "Pertemuan", - "waiting": "Menunggu pembawa acara...", - "linkInvalid": "Tautan tamu ini tidak valid atau rapat telah berakhir." - }, - "reopenFromPanel": "Tab rapat ini telah kedaluwarsa. Buka kembali ruangan dari panel Rapat.", - "requestControl": "Kontrol permintaan", - "cancelControlRequest": "Batalkan permintaan", - "controlQueue": "Permintaan kontrol", - "noControlRequests": "Tidak ada permintaan yang tertunda.", - "grantControlTo": "Berikan kendali ke {{name}}", - "dismissControlRequest": "Abaikan permintaan kontrol {{name}}", - "releaseControl": "Lepaskan kontrol", - "grantControl": "Berikan kendali", - "revokeControl": "Ambil kembali kendali", - "controlRequestedBy": "{{name}} meminta kendali", - "grant": "Menganugerahkan", - "controlBadge": "Dalam kendali", - "youArePresenting": "Anda sedang melakukan presentasi di tab lain. Hentikan presentasi untuk memberikan kesempatan kepada saya.", - "created": "Ruangan dibuat", - "invited": "Anggota diundang", - "roomTab": "Pertemuan" - }, "sessionSharing": { - "guestView": { - "loading": "Menghubungkan ke sesi bersama...", - "linkInvalid": "Tautan berbagi ini tidak valid, telah kedaluwarsa, atau telah dicabut.", - "rateLimited": "Terlalu banyak percobaan, silakan coba lagi sebentar lagi.", - "sessionEnded": "Sesi ini telah berakhir.", - "readOnlyBadge": "Hanya tampilan", - "ownerLabel": "Pemilik host" - }, - "modalTitle": "Sesi berbagi", - "shareButton": "Membagikan", - "notReadyToShare": "Sesi belum siap untuk dibagikan.", - "modeTab": { - "link": "Link", - "user": "Pengguna" - }, - "linkModeDescription": "Siapa pun yang memiliki tautan ini dapat bergabung, tidak perlu membuat akun.", - "userModeDescription": "Bagikan dengan pengguna tertentu yang sudah memiliki akses ke host ini. Jika mereka belum memiliki akses, bagikan host tersebut kepada mereka terlebih dahulu atau gunakan tautan. Setelah dibagikan, sesi akan muncul di tab Koneksi mereka.", "permissionLevel": { - "label": "Tingkat izin", "readOnly": "Hanya baca", - "readOnlyDescription": "Bisa menonton sesi secara langsung tetapi tidak bisa mengetik atau berinteraksi.", - "readWrite": "Baca-tulis", - "readWriteDescription": "Dapat mengetik dan berinteraksi dengan sesi seperti halnya pemilik." - }, - "expiryLabel": "Kedaluwarsa tautan", - "createLinkButton": "Buat tautan", - "createShareButton": "Bagikan dengan pengguna", - "searchUsersPlaceholder": "Cari pengguna...", - "noUsersFound": "Tidak ada pengguna yang ditemukan", - "linkCreated": "Tautan berbagi telah dibuat", - "linkCopied": "Tautan disalin ke papan klip", - "copyLink": "Salin tautan", - "shareCreated": "Sesi dibagikan. Sesi tersebut akan muncul di tab Koneksi mereka.", - "shareFailed": "Gagal membuat saham", - "userLacksHostAccess": "Pengguna tersebut belum memiliki akses ke host ini. Bagikan host tersebut kepada mereka terlebih dahulu, atau gunakan tautan sebagai gantinya.", - "activeShares": "Saham aktif", - "noActiveShares": "Tidak ada saham aktif untuk sesi ini.", - "revoke": "Menarik kembali", - "revokeConfirmTitle": "Batalkan pembagian ini?", - "revokeConfirmDescription": "Siapa pun yang menggunakan share ini akan langsung kehilangan akses.", - "revoked": "Saham dicabut", - "revokeFailed": "Gagal mencabut saham", - "joinCount": "{{count}} bergabung", - "joinCount_other": "{{count}} bergabung", - "expiresAt": "Berakhir {{date}}", - "linkShareBadge": "Link", - "userShareBadge": "Pengguna: {{username}}", - "loadSharesFailed": "Gagal memuat saham aktif." + "readWrite": "Baca-tulis" + } }, "terminal": { "connect": "Terhubung ke Host", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Mengirimkan data harian anonim mengenai jumlah pengguna, host, dan penggunaan fitur untuk membantu meningkatkan Termix. Tidak ada data pribadi atau detail koneksi yang disertakan.", "analyticsEnabledLockedDesc": "Pengaturan ini dikunci oleh variabel lingkungan ENABLE_TELEMETRY dan tidak dapat diubah di sini.", "updateAnalyticsFailed": "Gagal memperbarui pengaturan analitik.", - "sessionSharingGloballyEnabled": "Izinkan Berbagi Sesi", "auditForwardingTitle": "Teruskan ke SIEM", "auditForwardingDesc": "Kirim salinan setiap entri audit ke pengumpul data eksternal sebagai NDJSON. Log lokal tetap menjadi sumber kebenaran.", "auditForwardingEnvNotice": "Variabel lingkungan juga mengkonfigurasi penerusan; URL yang disimpan di sini akan diutamakan.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Token pembawa (tersimpan)", "auditForwardingSaved": "Pengaturan penerusan audit tersimpan", "auditForwardingSaveError": "Gagal menyimpan pengaturan penerusan audit", - "sessionSharingGloballyEnabledDesc": "Izinkan sesi terminal langsung, RDP, VNC, dan Telnet untuk dibagikan di seluruh instance. Menggantikan setiap pengaturan berbagi per host saat dinonaktifkan.", - "updateSessionSharingFailed": "Gagal memperbarui pengaturan berbagi sesi.", "sessionTimeout": "Waktu sesi habis", "hours": "jam", "sessionTimeoutRange": "Minimal 1 jam · Maksimal 720 jam", diff --git a/src/ui/locales/translated/it_IT.json b/src/ui/locales/translated/it_IT.json index df40e1901..8134cbc87 100644 --- a/src/ui/locales/translated/it_IT.json +++ b/src/ui/locales/translated/it_IT.json @@ -667,7 +667,6 @@ "done_cli_link": "Apri la documentazione della CLI" }, "nav": { - "collab": "Riunioni", "home": "Home", "terminal": "Terminale", "localTerminal": "Terminale locale", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Mantieni la tua shell e tutto ciò che è in esecuzione al suo interno attivi anche in caso di disconnessioni e riavvii del browser, eseguendoli all'interno di tmux (tmux deve essere installato sull'host).", "enableSessionLogging": "Registrazione sessione", "enableSessionLoggingDesc": "Registra l'output della sessione del terminale per la revisione successiva", - "allowSessionSharing": "Consenti la condivisione della sessione", - "allowSessionSharingDesc": "Consenti la condivisione delle sessioni live su questo host tramite link o con altri utenti", "localEchoLabel": "Eco locale", "localEchoDesc": "Prevedi localmente le sequenze di tasti sicure e stampabili anche con connessioni lente. Le richieste di password e gli input di controllo non vengono mai ripetuti.", "localEchoDefault": "Utilizza le impostazioni globali", @@ -1571,137 +1568,11 @@ "join": "Giuntura", "sharedSessionLabel": "{{hostName}} (condiviso)" }, - "collab": { - "createRoom": "Nuova stanza", - "roomName": "Nome della stanza", - "persistentRoom": "Stanza persistente", - "persistentRoomHint": "Le stanze persistenti rimangono elencate anche dopo la fine della riunione e possono essere riutilizzate.", - "noRooms": "Non ci sono ancora stanze. Creane una per avviare una riunione.", - "openRoom": "Aprire", - "endRoom": "Fine della riunione", - "leaveRoom": "Partire", - "deleteMember": "Rimuovere", - "removeMember": "Rimuovi {{name}}", - "removeMemberTitle": "Rimuovere questo membro?", - "removeMemberDescription": "{{name}} perderà immediatamente l'accesso a questa stanza. Qualsiasi visualizzatore SSH attivo verrà disconnesso.", - "invite": "Invitare", - "inviteTitle": "Invita i membri", - "members": "Membri", - "membersPanel": "Membri della stanza e richieste di controllo", - "membersWithCount": "Membri ({{count}})", - "online": "online", - "offline": "offline", - "you": "(Voi)", - "hostBadge": "Ospite", - "presenterBadge": "Presentazione", - "present": "Presente", - "presentTitle": "Sali sul palco", - "presentHost": "Ospite", - "searchHosts": "Host di ricerca", - "noHostsFound": "Nessun host disponibile trovato.", - "presentProtocol": "Protocollo", - "stopPresenting": "Smetti di presentare", - "takeOver": "Rilevare", - "takeOverConfirmTitle": "Sostituire la presentazione attuale?", - "takeOverDescription": "{{name}} sta presentando ora. Prendere il controllo disconnetterà tutti da quel palco.", - "emptyStage": "Nessuno sta presentando. Salite sul palco per condividere una sessione.", - "stageLoading": "Collegamento al palco...", - "stageEnded": "La presentazione è terminata", - "roomEnded": "La riunione è terminata", - "presenterLabel": "{{name}} sta presentando", - "guestLink": "Link ospite", - "guestLinkOn": "Il link per gli ospiti è attivo. Chiunque abbia il link può assistere alla diretta.", - "guestLinkOff": "Il link per gli ospiti è disattivato.", - "copyLink": "Copia il link", - "rotateLink": "Ruota il collegamento", - "disableLink": "Disabilita il collegamento", - "rotateLinkConfirmTitle": "Ruotare il link per gli ospiti?", - "rotateLinkDescription": "Il collegamento attuale smetterà di funzionare immediatamente. Gli ospiti SSH connessi verranno disconnessi.", - "disableLinkConfirmTitle": "Disabilitare il link per gli ospiti?", - "disableLinkDescription": "Gli utenti che tentano di connettersi tramite SSH in modo anonimo verranno disconnessi immediatamente e il collegamento corrente smetterà di funzionare.", - "linkCopied": "Link copiato", - "roles": "Ruoli", - "users": "Utenti", - "searchUsers": "Utenti di ricerca", - "endConfirmTitle": "Chiudere questa riunione?", - "endDescription": "{{name}} terminerà per tutti e non potrà essere riaperto.", - "endPersistentDescription": "La presentazione attiva in {{name}} verrà interrotta. La sala rimane disponibile per un altro utilizzo.", - "deleteRoom": "Eliminare la stanza", - "deleteConfirmTitle": "Eliminare questa stanza?", - "deleteDescription": "{{name}} verrà eliminato definitivamente per tutti e non sarà possibile annullare l'operazione.", - "invitedTo": "Sei stato invitato a \"{{name}}\"", - "guest": { - "title": "Incontro", - "waiting": "In attesa del presentatore...", - "linkInvalid": "Questo link per gli ospiti non è valido oppure la riunione è terminata." - }, - "reopenFromPanel": "La scheda di questa riunione è scaduta. Riapri la sala riunioni dal pannello Riunioni.", - "requestControl": "Controllo della richiesta", - "cancelControlRequest": "Annulla richiesta", - "controlQueue": "Richieste di controllo", - "noControlRequests": "Nessuna richiesta in sospeso.", - "grantControlTo": "Assegna il controllo a {{name}}", - "dismissControlRequest": "Ignora la richiesta di controllo di {{name}}", - "releaseControl": "Controllo del rilascio", - "grantControl": "Dare il controllo", - "revokeControl": "Riprendi il controllo", - "controlRequestedBy": "{{name}} ha chiesto il controllo", - "grant": "Concessione", - "controlBadge": "In controllo", - "youArePresenting": "Stai presentando in un'altra scheda. Interrompi la presentazione per cedere la parola.", - "created": "Stanza creata", - "invited": "Membri invitati", - "roomTab": "Incontro" - }, "sessionSharing": { - "guestView": { - "loading": "Connessione alla sessione condivisa in corso...", - "linkInvalid": "Questo link di condivisione non è valido, è scaduto o è stato revocato.", - "rateLimited": "Troppi tentativi, riprova tra poco.", - "sessionEnded": "Questa sessione è terminata", - "readOnlyBadge": "Solo visualizzazione", - "ownerLabel": "Proprietario dell'host" - }, - "modalTitle": "Sessione di condivisione", - "shareButton": "Condividere", - "notReadyToShare": "La sessione non è ancora pronta per la condivisione.", - "modeTab": { - "link": "Collegamento", - "user": "Utente" - }, - "linkModeDescription": "Chiunque abbia questo link può partecipare, non è necessario creare un account.", - "userModeDescription": "Condividi la sessione con un utente specifico che ha già accesso a questo host. Se non ha ancora accesso, condividi prima l'host con lui oppure usa un link. Una volta condivisa, la sessione apparirà nella sua scheda Connessioni.", "permissionLevel": { - "label": "Livello di autorizzazione", "readOnly": "Sola lettura", - "readOnlyDescription": "È possibile seguire la sessione in diretta, ma non è possibile digitare o interagire.", - "readWrite": "lettura-scrittura", - "readWriteDescription": "Può digitare e interagire con la sessione proprio come il proprietario." - }, - "expiryLabel": "Scadenza del collegamento", - "createLinkButton": "Crea collegamento", - "createShareButton": "Condividi con l'utente", - "searchUsersPlaceholder": "Cerca utenti...", - "noUsersFound": "Nessun utente trovato", - "linkCreated": "Link di condivisione creato", - "linkCopied": "Link copiato negli appunti", - "copyLink": "Copia il link", - "shareCreated": "Sessione condivisa. Sarà visibile nella scheda Connessioni.", - "shareFailed": "Impossibile creare la condivisione", - "userLacksHostAccess": "Quell'utente non ha ancora accesso a questo host. Condividi prima l'host con lui, oppure usa un link.", - "activeShares": "Condivisioni attive", - "noActiveShares": "Nessuna azione attiva in questa sessione.", - "revoke": "Revocare", - "revokeConfirmTitle": "Revocare questa condivisione?", - "revokeConfirmDescription": "Chiunque utilizzi questa condivisione perderà immediatamente l'accesso.", - "revoked": "Azione revocata.", - "revokeFailed": "Impossibile revocare la quota", - "joinCount": "{{count}} unisciti", - "joinCount_other": "{{count}} si unisce", - "expiresAt": "Scade il {{date}}", - "linkShareBadge": "Collegamento", - "userShareBadge": "Utente: {{username}}", - "loadSharesFailed": "Impossibile caricare le azioni attive" + "readWrite": "lettura-scrittura" + } }, "terminal": { "connect": "Connetti all'host", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Invia un conteggio giornaliero anonimo di utenti, host e utilizzo delle funzionalità per contribuire al miglioramento di Termix. Non vengono mai inclusi dati personali o dettagli di connessione.", "analyticsEnabledLockedDesc": "Questa impostazione è bloccata dalla variabile d'ambiente ENABLE_TELEMETRY e non può essere modificata qui.", "updateAnalyticsFailed": "Impossibile aggiornare le impostazioni di analisi", - "sessionSharingGloballyEnabled": "Consenti la condivisione della sessione", "auditForwardingTitle": "Inoltra a SIEM", "auditForwardingDesc": "Invia una copia di ogni voce di audit a un raccoglitore esterno in formato NDJSON. I log locali rimangono la fonte di verità.", "auditForwardingEnvNotice": "Una variabile d'ambiente configura anche l'inoltro delle richieste; l'URL salvato in questa variabile ha la precedenza.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Token al portatore (conservato)", "auditForwardingSaved": "Impostazioni di inoltro del controllo salvate", "auditForwardingSaveError": "Impossibile salvare le impostazioni di inoltro del controllo.", - "sessionSharingGloballyEnabledDesc": "Consente la condivisione di sessioni live di terminale, RDP, VNC e Telnet a livello di istanza. Quando disabilitata, questa opzione sovrascrive tutte le impostazioni di condivisione per singolo host.", - "updateSessionSharingFailed": "Impossibile aggiornare le impostazioni di condivisione della sessione", "sessionTimeout": "Timeout sessione", "hours": "ore", "sessionTimeoutRange": "Min 1h · Max 720h", diff --git a/src/ui/locales/translated/ja_JP.json b/src/ui/locales/translated/ja_JP.json index 5dba65ddd..e8ed52662 100644 --- a/src/ui/locales/translated/ja_JP.json +++ b/src/ui/locales/translated/ja_JP.json @@ -667,7 +667,6 @@ "done_cli_link": "CLIドキュメントを開く" }, "nav": { - "collab": "会議", "home": "ホーム", "terminal": "ターミナル", "localTerminal": "ローカル端末", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "tmux内で実行することで、接続切断やブラウザの再起動後もシェルとその中で実行されているすべての処理を正常に実行状態に保つことができます(ホストにtmuxがインストールされている必要があります)。", "enableSessionLogging": "セッションログ", "enableSessionLoggingDesc": "後で確認できるようにターミナルセッションの出力を記録します", - "allowSessionSharing": "セッション共有を許可する", - "allowSessionSharingDesc": "このホスト上のライブセッションをリンクまたは他のユーザーと共有できるようにします。", "localEchoLabel": "ローカルエコー", "localEchoDesc": "低速回線でも、印刷可能な安全なキー入力をローカルで予測します。パスワード入力や制御入力はエコーバックされません。", "localEchoDefault": "グローバル設定を使用する", @@ -1571,137 +1568,11 @@ "join": "参加する", "sharedSessionLabel": "{{hostName}} (共有)" }, - "collab": { - "createRoom": "新しい部屋", - "roomName": "部屋名", - "persistentRoom": "永続的な部屋", - "persistentRoomHint": "永続的な会議室は、会議終了後もリストに表示され、再利用できます。", - "noRooms": "まだ会議室がありません。会議室を作成して会議を開始してください。", - "openRoom": "開ける", - "endRoom": "会議終了", - "leaveRoom": "離れる", - "deleteMember": "取り除く", - "removeMember": "{{name}} を削除", - "removeMemberTitle": "このメンバーを削除しますか?", - "removeMemberDescription": "{{name}} は直ちにこのルームへのアクセス権を失います。アクティブな SSH ビューアはすべて切断されます。", - "invite": "招待する", - "inviteTitle": "メンバーを招待する", - "members": "メンバー", - "membersPanel": "ルームメンバーと制御リクエスト", - "membersWithCount": "メンバー({{count}})", - "online": "オンライン", - "offline": "オフライン", - "you": "(あなた)", - "hostBadge": "ホスト", - "presenterBadge": "発表します", - "present": "現在", - "presentTitle": "ステージに立つ", - "presentHost": "ホスト", - "searchHosts": "ホストを検索", - "noHostsFound": "利用可能なホストが見つかりませんでした。", - "presentProtocol": "プロトコル", - "stopPresenting": "プレゼンテーションをやめる", - "takeOver": "引き継ぐ", - "takeOverConfirmTitle": "現在のプレゼンテーションを置き換えますか?", - "takeOverDescription": "{{name}} が現在プレゼンテーションを行っています。引き継ぐと、そのステージから全員が切断されます。", - "emptyStage": "発表者はいません。ステージに上がってセッションを共有してください。", - "stageLoading": "ステージに接続中…", - "stageEnded": "プレゼンテーションは終了しました", - "roomEnded": "この会議は終了しました。", - "presenterLabel": "{{name}} がプレゼンテーションを行っています", - "guestLink": "ゲストリンク", - "guestLinkOn": "ゲストリンクが有効になっています。リンクを知っている人なら誰でもステージを視聴できます。", - "guestLinkOff": "ゲストリンクが無効になっています。", - "copyLink": "リンクをコピー", - "rotateLink": "リンクを回転させる", - "disableLink": "リンクを無効にする", - "rotateLinkConfirmTitle": "ゲストリンクを回転させますか?", - "rotateLinkDescription": "現在の接続は直ちに停止します。接続中のSSHゲストは切断されます。", - "disableLinkConfirmTitle": "ゲストリンクを無効にしますか?", - "disableLinkDescription": "匿名SSHビューアは直ちに切断され、現在の接続は機能しなくなります。", - "linkCopied": "リンクをコピーしました", - "roles": "役割", - "users": "ユーザー", - "searchUsers": "ユーザーを検索", - "endConfirmTitle": "この会議を終了しますか?", - "endDescription": "{{name}} は全員に対して終了し、再開することはできません。", - "endPersistentDescription": "{{name}} で現在行われているプレゼンテーションは終了します。この部屋は引き続きご利用いただけます。", - "deleteRoom": "ルームを削除", - "deleteConfirmTitle": "この部屋を削除しますか?", - "deleteDescription": "{{name}} は全員に対して完全に削除され、元に戻すことはできません。", - "invitedTo": "あなたは「{{name}}」に招待されました。", - "guest": { - "title": "ミーティング", - "waiting": "プレゼンターをお待ちしています…", - "linkInvalid": "このゲストリンクは無効であるか、ミーティングが終了しています" - }, - "reopenFromPanel": "この会議タブの有効期限が切れました。会議パネルから会議室を再度開いてください。", - "requestControl": "リクエスト制御", - "cancelControlRequest": "キャンセルリクエスト", - "controlQueue": "制御要求", - "noControlRequests": "保留中のリクエストはありません。", - "grantControlTo": "{{name}}に制御権を与える", - "dismissControlRequest": "{{name}}の制御要求を却下します", - "releaseControl": "リリース制御", - "grantControl": "コントロールを与える", - "revokeControl": "主導権を取り戻す", - "controlRequestedBy": "{{name}} が制御を要求しました", - "grant": "付与", - "controlBadge": "制御する", - "youArePresenting": "あなたは別のタブでプレゼンテーションを行っています。プレゼンテーションを中断して、ステージを譲ってください。", - "created": "ルームが作成されました", - "invited": "会員の皆様を招待いたします", - "roomTab": "ミーティング" - }, "sessionSharing": { - "guestView": { - "loading": "共有セッションに接続しています...", - "linkInvalid": "この共有リンクは無効、期限切れ、または取り消されています。", - "rateLimited": "試行回数が多すぎます。しばらくしてからもう一度お試しください。", - "sessionEnded": "このセッションは終了しました", - "readOnlyBadge": "閲覧のみ", - "ownerLabel": "ホストオーナー" - }, - "modalTitle": "セッションを共有する", - "shareButton": "共有", - "notReadyToShare": "セッションはまだ共有できません", - "modeTab": { - "link": "リンク", - "user": "ユーザー" - }, - "linkModeDescription": "このリンクを知っている人なら誰でも参加できます。アカウントは必要ありません。", - "userModeDescription": "このホストへのアクセス権を既に持っている特定のユーザーと共有します。まだアクセス権がない場合は、まずホストを共有するか、代わりにリンクを使用してください。共有が完了すると、セッションはユーザーの「接続」タブに表示されます。", "permissionLevel": { - "label": "権限レベル", "readOnly": "読み取り専用", - "readOnlyDescription": "セッションをライブで視聴することはできますが、文字入力やインタラクションはできません。", - "readWrite": "読み書き", - "readWriteDescription": "所有者と同様に、セッション内で文字を入力したり操作したりできます。" - }, - "expiryLabel": "リンクの有効期限", - "createLinkButton": "リンクを作成する", - "createShareButton": "ユーザーと共有する", - "searchUsersPlaceholder": "ユーザーを検索...", - "noUsersFound": "ユーザーが見つかりませんでした", - "linkCreated": "共有リンクが作成されました", - "linkCopied": "リンクがクリップボードにコピーされました", - "copyLink": "リンクをコピー", - "shareCreated": "セッションが共有されました。相手の「接続」タブに表示されます。", - "shareFailed": "共有の作成に失敗しました", - "userLacksHostAccess": "そのユーザーはまだこのホストへのアクセス権を持っていません。まずホストを共有するか、代わりにリンクを使用してください。", - "activeShares": "アクティブなシェア", - "noActiveShares": "今セッションで有効な株式はありません", - "revoke": "取り消す", - "revokeConfirmTitle": "この共有を取り消しますか?", - "revokeConfirmDescription": "この共有ファイルを使用している人は、即座にアクセス権を失います。", - "revoked": "シェアが取り消されました", - "revokeFailed": "シェアの取り消しに失敗しました", - "joinCount": "{{count}} 参加する", - "joinCount_other": "{{count}} が参加します", - "expiresAt": "有効期限 {{date}}", - "linkShareBadge": "リンク", - "userShareBadge": "ユーザー: {{username}}", - "loadSharesFailed": "アクティブな共有の読み込みに失敗しました" + "readWrite": "読み書き" + } }, "terminal": { "connect": "ホストに接続", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Termixの改善に役立てるため、ユーザー数、ホスト数、機能使用状況に関する匿名データを毎日送信します。個人データや接続情報は一切含まれません。", "analyticsEnabledLockedDesc": "この設定は環境変数ENABLE_TELEMETRYによってロックされているため、ここで変更することはできません。", "updateAnalyticsFailed": "分析設定の更新に失敗しました", - "sessionSharingGloballyEnabled": "セッション共有を許可する", "auditForwardingTitle": "SIEMへ転送", "auditForwardingDesc": "すべての監査エントリのコピーをNDJSON形式で外部の収集業者に送信します。ローカルログは真の情報源として保持されます。", "auditForwardingEnvNotice": "環境変数でも転送設定を行うことができます。ここに保存されたURLが優先されます。", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "ベアラートークン(保存済み)", "auditForwardingSaved": "監査転送設定が保存されました", "auditForwardingSaveError": "監査転送設定の保存に失敗しました", - "sessionSharingGloballyEnabledDesc": "ライブターミナル、RDP、VNC、およびTelnetセッションをインスタンス全体で共有できるようにします。無効にすると、ホストごとの共有設定がすべて上書きされます。", - "updateSessionSharingFailed": "セッション共有設定の更新に失敗しました", "sessionTimeout": "セッションタイムアウト", "hours": "時間", "sessionTimeoutRange": "最小1時間・最大720時間", diff --git a/src/ui/locales/translated/ko_KR.json b/src/ui/locales/translated/ko_KR.json index a7f23601f..2a828138e 100644 --- a/src/ui/locales/translated/ko_KR.json +++ b/src/ui/locales/translated/ko_KR.json @@ -667,7 +667,6 @@ "done_cli_link": "CLI 문서를 엽니다." }, "nav": { - "collab": "회의", "home": "홈", "terminal": "터미널", "localTerminal": "로컬 터미널", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "tmux 내부에서 실행하면 연결이 끊기거나 브라우저가 다시 시작되더라도 셸과 그 안에서 실행 중인 모든 작업이 계속 유지됩니다(호스트에 tmux가 설치되어 있어야 함).", "enableSessionLogging": "세션 로깅", "enableSessionLoggingDesc": "나중에 검토할 수 있도록 터미널 세션 출력을 기록합니다", - "allowSessionSharing": "세션 공유 허용", - "allowSessionSharingDesc": "이 호스트에서 진행되는 라이브 세션을 링크 또는 다른 사용자와 공유하세요.", "localEchoLabel": "로컬 에코", "localEchoDesc": "연결 속도가 느린 환경에서도 안전하게 입력 가능한 키 입력을 로컬에서 예측합니다. 비밀번호 입력 및 제어 입력은 절대 화면에 표시되지 않습니다.", "localEchoDefault": "전역 설정을 사용하세요", @@ -1571,137 +1568,11 @@ "join": "가입하다", "sharedSessionLabel": "{{hostName}} (공유됨)" }, - "collab": { - "createRoom": "새로운 방", - "roomName": "객실 이름", - "persistentRoom": "지속적인 방", - "persistentRoomHint": "회의가 끝난 후에도 지정된 회의실 목록에 남아 재사용할 수 있습니다.", - "noRooms": "아직 회의실이 없습니다. 회의를 시작하려면 회의실을 만드세요.", - "openRoom": "열려 있는", - "endRoom": "회의 종료", - "leaveRoom": "떠나다", - "deleteMember": "제거하다", - "removeMember": "{{name}}를 제거하세요", - "removeMemberTitle": "이 회원을 삭제하시겠습니까?", - "removeMemberDescription": "{{name}} 사용자는 즉시 이 방에 대한 접근 권한을 잃게 됩니다. 활성화된 SSH 뷰어는 모두 연결이 끊어집니다.", - "invite": "초대하다", - "inviteTitle": "회원 초대", - "members": "회원들", - "membersPanel": "회의실 구성원 및 제어 요청", - "membersWithCount": "멤버({{count}})", - "online": "온라인", - "offline": "오프라인", - "you": "(너)", - "hostBadge": "주인", - "presenterBadge": "소개합니다", - "present": "현재의", - "presentTitle": "무대에 오르세요", - "presentHost": "주인", - "searchHosts": "호스트 검색", - "noHostsFound": "사용 가능한 호스트를 찾을 수 없습니다.", - "presentProtocol": "규약", - "stopPresenting": "프레젠테이션을 중단하세요", - "takeOver": "인수하세요", - "takeOverConfirmTitle": "현재 프레젠테이션을 교체하시겠습니까?", - "takeOverDescription": "{{name}} 님이 현재 발표 중입니다. 발표를 이어받으면 해당 무대에서 모든 사용자의 연결이 끊어집니다.", - "emptyStage": "발표자가 없습니다. 무대에 올라 세션을 공유해 보세요.", - "stageLoading": "무대와 연결 중...", - "stageEnded": "프레젠테이션이 끝났습니다.", - "roomEnded": "이 회의는 종료되었습니다.", - "presenterLabel": "{{name}} 가 발표합니다", - "guestLink": "게스트 링크", - "guestLinkOn": "게스트 링크가 활성화되었습니다. 링크만 있으면 누구나 무대를 시청할 수 있습니다.", - "guestLinkOff": "게스트 링크가 꺼져 있습니다.", - "copyLink": "링크 복사", - "rotateLink": "링크를 회전하세요", - "disableLink": "링크 비활성화", - "rotateLinkConfirmTitle": "게스트 링크를 변경하시겠습니까?", - "rotateLinkDescription": "현재 연결은 즉시 작동을 멈춥니다. 연결된 SSH 게스트는 연결이 끊어집니다.", - "disableLinkConfirmTitle": "게스트 링크를 비활성화하시겠습니까?", - "disableLinkDescription": "익명 SSH 뷰어는 즉시 연결이 끊어지고 현재 링크가 작동을 멈춥니다.", - "linkCopied": "링크 복사됨", - "roles": "역할", - "users": "사용자", - "searchUsers": "검색 사용자", - "endConfirmTitle": "이 회의를 종료하시겠습니까?", - "endDescription": "{{name}} 는 모든 사용자에게 종료되며 다시 열 수 없습니다.", - "endPersistentDescription": "{{name}} 에서 진행 중이던 프레젠테이션이 종료됩니다. 해당 공간은 재사용 가능합니다.", - "deleteRoom": "방 삭제", - "deleteConfirmTitle": "이 방을 삭제하시겠습니까?", - "deleteDescription": "{{name}} 는 모든 사용자에게 영구적으로 삭제되며 되돌릴 수 없습니다.", - "invitedTo": "당신은 \"{{name}} \"에 초대되었습니다.", - "guest": { - "title": "회의", - "waiting": "발표자를 기다리는 중...", - "linkInvalid": "게스트 링크가 유효하지 않거나 회의가 종료되었습니다." - }, - "reopenFromPanel": "이 회의 탭이 만료되었습니다. 회의 패널에서 회의실을 다시 여세요.", - "requestControl": "요청 제어", - "cancelControlRequest": "취소 요청", - "controlQueue": "제어 요청", - "noControlRequests": "보류 중인 요청이 없습니다.", - "grantControlTo": "{{name}}에 제어권을 넘겨주세요", - "dismissControlRequest": "{{name}}의 제어 요청을 기각합니다.", - "releaseControl": "릴리스 컨트롤", - "grantControl": "제어권을 넘겨주세요", - "revokeControl": "주도권을 되찾으세요", - "controlRequestedBy": "{{name}} 제어권을 요청했습니다.", - "grant": "승인하다", - "controlBadge": "통제하에", - "youArePresenting": "다른 탭에서 프레젠테이션을 진행하고 계십니다. 프레젠테이션을 중지하고 다음 사람에게 넘겨주세요.", - "created": "방이 생성되었습니다", - "invited": "회원 초대", - "roomTab": "회의" - }, "sessionSharing": { - "guestView": { - "loading": "공유 세션에 연결 중...", - "linkInvalid": "이 공유 링크는 유효하지 않거나, 만료되었거나, 취소되었습니다.", - "rateLimited": "시도 횟수가 너무 많습니다. 잠시 후 다시 시도해 주세요.", - "sessionEnded": "이번 세션이 종료되었습니다.", - "readOnlyBadge": "보기 전용", - "ownerLabel": "호스트 소유자" - }, - "modalTitle": "공유 세션", - "shareButton": "공유하다", - "notReadyToShare": "세션을 공유할 준비가 아직 되지 않았습니다.", - "modeTab": { - "link": "링크", - "user": "사용자" - }, - "linkModeDescription": "이 링크만 있으면 누구나 가입할 수 있으며, 계정은 필요하지 않습니다.", - "userModeDescription": "이미 해당 호스트에 접근 권한이 있는 특정 사용자와 공유하세요. 접근 권한이 없는 경우, 먼저 호스트를 공유하거나 링크를 사용하세요. 공유가 완료되면 해당 세션이 사용자의 '연결' 탭에 나타납니다.", "permissionLevel": { - "label": "권한 수준", "readOnly": "읽기 전용", - "readOnlyDescription": "실시간으로 세션을 시청할 수는 있지만, 입력하거나 상호 작용할 수는 없습니다.", - "readWrite": "읽기-쓰기", - "readWriteDescription": "소유자와 마찬가지로 세션에 입력하고 상호 작용할 수 있습니다." - }, - "expiryLabel": "링크 만료", - "createLinkButton": "링크 생성", - "createShareButton": "사용자와 공유", - "searchUsersPlaceholder": "사용자 검색...", - "noUsersFound": "사용자를 찾을 수 없습니다.", - "linkCreated": "공유 링크가 생성되었습니다", - "linkCopied": "링크가 클립보드에 복사되었습니다", - "copyLink": "링크 복사", - "shareCreated": "세션이 공유되었습니다. 상대방의 연결 탭에 표시됩니다.", - "shareFailed": "공유 폴더 생성에 실패했습니다.", - "userLacksHostAccess": "해당 사용자는 아직 이 호스트에 대한 접근 권한이 없습니다. 먼저 호스트를 공유하거나 링크를 사용하세요.", - "activeShares": "활성 주식", - "noActiveShares": "이번 세션에는 활성 주식이 없습니다.", - "revoke": "취소", - "revokeConfirmTitle": "이 지분을 취소하시겠습니까?", - "revokeConfirmDescription": "이 공유 폴더를 사용하는 사람은 누구든 즉시 접근 권한을 잃게 됩니다.", - "revoked": "공유 취소됨", - "revokeFailed": "공유 취소에 실패했습니다", - "joinCount": "{{count}} 참여", - "joinCount_other": "{{count}} 참여", - "expiresAt": "만료일 {{date}}", - "linkShareBadge": "링크", - "userShareBadge": "사용자: {{username}}", - "loadSharesFailed": "활성 공유 항목을 로드하는 데 실패했습니다." + "readWrite": "읽기-쓰기" + } }, "terminal": { "connect": "호스트에 연결", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Termix 개선을 위해 사용자, 호스트 및 기능 사용량에 대한 익명의 일일 통계를 전송합니다. 개인 정보나 연결 정보는 절대 포함되지 않습니다.", "analyticsEnabledLockedDesc": "이 설정은 ENABLE_TELEMETRY 환경 변수에 의해 잠겨 있으므로 여기에서 변경할 수 없습니다.", "updateAnalyticsFailed": "분석 설정 업데이트에 실패했습니다.", - "sessionSharingGloballyEnabled": "세션 공유 허용", "auditForwardingTitle": "SIEM으로 전달", "auditForwardingDesc": "모든 감사 항목의 사본을 NDJSON 형식으로 외부 수집기에 전송합니다. 로컬 로그가 원본 데이터의 원천으로 유지됩니다.", "auditForwardingEnvNotice": "환경 변수를 통해 포워딩을 설정할 수도 있으며, 여기에 저장된 URL이 우선 적용됩니다.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "베어러 토큰(저장됨)", "auditForwardingSaved": "감사 전달 설정이 저장되었습니다.", "auditForwardingSaveError": "감사 전달 설정을 저장하는 데 실패했습니다.", - "sessionSharingGloballyEnabledDesc": "라이브 터미널, RDP, VNC 및 Telnet 세션을 인스턴스 전체에서 공유할 수 있도록 허용합니다. 이 기능을 비활성화하면 호스트별 공유 설정이 모두 무시됩니다.", - "updateSessionSharingFailed": "세션 공유 설정 업데이트에 실패했습니다.", "sessionTimeout": "세션 타임아웃", "hours": "시간", "sessionTimeoutRange": "최소 1시간 · 최대 720시간", diff --git a/src/ui/locales/translated/nl_NL.json b/src/ui/locales/translated/nl_NL.json index 0baf81389..10d946c9a 100644 --- a/src/ui/locales/translated/nl_NL.json +++ b/src/ui/locales/translated/nl_NL.json @@ -667,7 +667,6 @@ "done_cli_link": "Open de CLI-documentatie." }, "nav": { - "collab": "Vergaderingen", "home": "Thuis", "terminal": "Terminal", "localTerminal": "Lokale terminal", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Zorg ervoor dat je shell en alles wat daarin draait, blijft functioneren, ook na een verbindingsoverbreking of een herstart van de browser, door tmux te gebruiken (tmux moet wel op de host geïnstalleerd zijn).", "enableSessionLogging": "Sessieregistratie", "enableSessionLoggingDesc": "Sla de uitvoer van de terminalsessie op voor latere beoordeling.", - "allowSessionSharing": "Sessiedeling toestaan", - "allowSessionSharingDesc": "Sta toe dat live sessies op deze host via een link of met andere gebruikers worden gedeeld.", "localEchoLabel": "Local Echo", "localEchoDesc": "Voorspel lokaal veilige toetsaanslagen, ook bij trage internetverbindingen. Wachtwoordprompts en besturingsinvoer worden nooit weergegeven.", "localEchoDefault": "Gebruik de globale instelling", @@ -1571,137 +1568,11 @@ "join": "Meedoen", "sharedSessionLabel": "{{hostName}} (gedeeld)" }, - "collab": { - "createRoom": "Nieuwe kamer", - "roomName": "Kamernaam", - "persistentRoom": "Permanente kamer", - "persistentRoomHint": "Kamers die permanent gereserveerd blijven, blijven na afloop van de vergadering beschikbaar en kunnen opnieuw worden gebruikt.", - "noRooms": "Er zijn nog geen vergaderruimtes. Maak er een aan om een vergadering te starten.", - "openRoom": "Open", - "endRoom": "Einde vergadering", - "leaveRoom": "Vertrekken", - "deleteMember": "Verwijderen", - "removeMember": "Verwijder {{name}}", - "removeMemberTitle": "Dit lid verwijderen?", - "removeMemberDescription": "{{name}} verliest onmiddellijk de toegang tot deze ruimte. Elke actieve SSH-viewer wordt losgekoppeld.", - "invite": "Uitnodiging", - "inviteTitle": "Nodig leden uit", - "members": "Leden", - "membersPanel": "Kamerleden en beheerverzoeken", - "membersWithCount": "Leden ({{count}})", - "online": "online", - "offline": "offline", - "you": "(Jij)", - "hostBadge": "Gastheer", - "presenterBadge": "Presentatie", - "present": "Cadeau", - "presentTitle": "Betreed het podium", - "presentHost": "Gastheer", - "searchHosts": "Zoek hosts", - "noHostsFound": "Geen beschikbare hosts gevonden.", - "presentProtocol": "Protocol", - "stopPresenting": "Stop met presenteren", - "takeOver": "Neem het over", - "takeOverConfirmTitle": "De huidige presentatie vervangen?", - "takeOverDescription": "{{name}} presenteert nu. Als je het overneemt, wordt iedereen van dat podium losgekoppeld.", - "emptyStage": "Niemand geeft een presentatie. Betreed het podium om een sessie te delen.", - "stageLoading": "Verbinding maken met het podium...", - "stageEnded": "De presentatie eindigde.", - "roomEnded": "Deze vergadering is afgelopen.", - "presenterLabel": "{{name}} presenteert", - "guestLink": "Gastlink", - "guestLinkOn": "De gastlink is ingeschakeld. Iedereen met de link kan het optreden bekijken.", - "guestLinkOff": "De gastlink is uitgeschakeld.", - "copyLink": "Kopieer link", - "rotateLink": "Draai de link", - "disableLink": "Link uitschakelen", - "rotateLinkConfirmTitle": "De gastlink roteren?", - "rotateLinkDescription": "De huidige verbinding wordt onmiddellijk verbroken. Verbonden SSH-gasten worden losgekoppeld.", - "disableLinkConfirmTitle": "De gastlink uitschakelen?", - "disableLinkDescription": "Anonieme SSH-gebruikers worden onmiddellijk losgekoppeld en de huidige verbinding zal niet meer werken.", - "linkCopied": "Link gekopieerd", - "roles": "Rollen", - "users": "Gebruikers", - "searchUsers": "Zoek gebruikers", - "endConfirmTitle": "Deze vergadering beëindigen?", - "endDescription": "{{name}} zal voor iedereen eindigen en kan niet opnieuw worden geopend.", - "endPersistentDescription": "De actieve presentatie in {{name}} wordt gestopt. De ruimte blijft beschikbaar voor hergebruik.", - "deleteRoom": "Verwijder kamer", - "deleteConfirmTitle": "Deze kamer verwijderen?", - "deleteDescription": "{{name}} wordt permanent verwijderd voor iedereen en kan niet ongedaan worden gemaakt.", - "invitedTo": "Je werd uitgenodigd voor \"{{name}}\"", - "guest": { - "title": "Ontmoeting", - "waiting": "Wachten op de presentator...", - "linkInvalid": "Deze gastlink is ongeldig of de vergadering is afgelopen." - }, - "reopenFromPanel": "Dit vergadertabblad is verlopen. Heropen de ruimte via het paneel Vergaderingen.", - "requestControl": "Verzoekbeheer", - "cancelControlRequest": "Verzoek annuleren", - "controlQueue": "Controleverzoeken", - "noControlRequests": "Geen openstaande aanvragen.", - "grantControlTo": "Geef de controle aan {{name}}", - "dismissControlRequest": "Negeer het controleverzoek van {{name}}", - "releaseControl": "Ontgrendeling", - "grantControl": "Geef de controle", - "revokeControl": "Neem de controle terug", - "controlRequestedBy": "{{name}} vroeg om controle", - "grant": "Studiebeurs", - "controlBadge": "Onder controle", - "youArePresenting": "Je presenteert momenteel in een ander tabblad. Stop met presenteren en geef het woord aan iemand anders.", - "created": "Kamer gecreëerd", - "invited": "Leden uitgenodigd", - "roomTab": "Ontmoeting" - }, "sessionSharing": { - "guestView": { - "loading": "Verbinding maken met de gedeelde sessie...", - "linkInvalid": "Deze deelbare link is ongeldig, verlopen of ingetrokken.", - "rateLimited": "Te veel pogingen, probeer het over een tijdje opnieuw.", - "sessionEnded": "Deze sessie is afgelopen.", - "readOnlyBadge": "Alleen bekijken", - "ownerLabel": "Gastheer/gastvrouw" - }, - "modalTitle": "Deel sessie", - "shareButton": "Deel", - "notReadyToShare": "De sessie is nog niet klaar om te delen.", - "modeTab": { - "link": "Link", - "user": "Gebruiker" - }, - "linkModeDescription": "Iedereen met deze link kan deelnemen, een account is niet nodig.", - "userModeDescription": "Deel de host met een specifieke gebruiker die er al toegang toe heeft. Als die gebruiker nog geen toegang heeft, deel de host dan eerst met hem of haar, of gebruik een link. Zodra de sessie is gedeeld, verschijnt deze in het tabblad 'Verbindingen' van de gebruiker.", "permissionLevel": { - "label": "Toegangsniveau", "readOnly": "Alleen-lezen", - "readOnlyDescription": "Je kunt de sessie live volgen, maar je kunt niet typen of interageren.", - "readWrite": "Lezen en schrijven", - "readWriteDescription": "Kan typen en interactie hebben met de sessie, net als de eigenaar." - }, - "expiryLabel": "Link verloopt", - "createLinkButton": "Link maken", - "createShareButton": "Delen met gebruiker", - "searchUsersPlaceholder": "Gebruikers doorzoeken...", - "noUsersFound": "Geen gebruikers gevonden", - "linkCreated": "Deellink aangemaakt", - "linkCopied": "Link gekopieerd naar het klembord.", - "copyLink": "Kopieer link", - "shareCreated": "De sessie is gedeeld. Deze verschijnt in hun tabblad 'Verbindingen'.", - "shareFailed": "Aanmaken van de share is mislukt", - "userLacksHostAccess": "Die gebruiker heeft nog geen toegang tot deze host. Deel de host eerst met hem of haar, of gebruik een link.", - "activeShares": "Actieve aandelen", - "noActiveShares": "Geen actieve aandelen voor deze sessie.", - "revoke": "Herroepen", - "revokeConfirmTitle": "Wilt u dit aandeel intrekken?", - "revokeConfirmDescription": "Iedereen die deze gedeelde map gebruikt, verliest onmiddellijk de toegang.", - "revoked": "Aandeel ingetrokken", - "revokeFailed": "Het intrekken van het aandeel is mislukt.", - "joinCount": "{{count}} join", - "joinCount_other": "{{count}} sluit zich aan", - "expiresAt": "Verloopt {{date}}", - "linkShareBadge": "Link", - "userShareBadge": "Gebruiker: {{username}}", - "loadSharesFailed": "Het laden van actieve aandelen is mislukt." + "readWrite": "Lezen en schrijven" + } }, "terminal": { "connect": "Verbinden met de host", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Verstuurt dagelijks anoniem een telling van gebruikers, hosts en functiegebruik om Termix te verbeteren. Er worden nooit persoonlijke gegevens of verbindingsdetails meegestuurd.", "analyticsEnabledLockedDesc": "Deze instelling is vergrendeld door de omgevingsvariabele ENABLE_TELEMETRY en kan hier niet worden gewijzigd.", "updateAnalyticsFailed": "Het bijwerken van de analyse-instellingen is mislukt.", - "sessionSharingGloballyEnabled": "Sessiedeling toestaan", "auditForwardingTitle": "Doorsturen naar SIEM", "auditForwardingDesc": "Verzend een kopie van elke auditlogboekvermelding naar een externe verzamelaar als NDJSON-bestand. Lokale logbestanden blijven de bron van waarheid.", "auditForwardingEnvNotice": "Een omgevingsvariabele configureert ook het doorsturen; de URL die hier is opgeslagen, heeft voorrang.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Token met houder (opgeslagen)", "auditForwardingSaved": "Audit-doorstuurinstellingen opgeslagen", "auditForwardingSaveError": "Het opslaan van de instellingen voor het doorsturen van auditlogboeken is mislukt.", - "sessionSharingGloballyEnabledDesc": "Sta toe dat live terminal-, RDP-, VNC- en Telnet-sessies instantiebreed worden gedeeld. Deze functie overschrijft elke instelling voor delen per host wanneer deze is uitgeschakeld.", - "updateSessionSharingFailed": "Het bijwerken van de instelling voor het delen van sessies is mislukt.", "sessionTimeout": "Sessietime-out", "hours": "uren", "sessionTimeoutRange": "Minimaal 1 uur · Maximaal 720 uur", diff --git a/src/ui/locales/translated/no_NO.json b/src/ui/locales/translated/no_NO.json index 2a35f8bb2..5966a46c4 100644 --- a/src/ui/locales/translated/no_NO.json +++ b/src/ui/locales/translated/no_NO.json @@ -667,7 +667,6 @@ "done_cli_link": "Åpne CLI-dokumentasjonen" }, "nav": { - "collab": "Møter", "home": "Hjem", "terminal": "Terminal", "localTerminal": "Lokal terminal", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Hold skallet ditt og alt som kjører i det i live på tvers av frakoblinger og omstart av nettleseren ved å kjøre det i tmux (tmux må være installert på verten)", "enableSessionLogging": "Øktlogging", "enableSessionLoggingDesc": "Ta opp utdata fra terminaløkten for senere gjennomgang", - "allowSessionSharing": "Tillat øktdeling", - "allowSessionSharingDesc": "La direktesendte økter på denne verten deles via lenke eller med andre brukere", "localEchoLabel": "Lokalt ekko", "localEchoDesc": "Forutsi trygge utskrivbare tastetrykk lokalt på trege tilkoblinger. Passordforespørsler og kontrollinndata gjentas aldri.", "localEchoDefault": "Bruk global innstilling", @@ -1571,137 +1568,11 @@ "join": "Bli med", "sharedSessionLabel": "{{hostName}} (delt)" }, - "collab": { - "createRoom": "Nytt rom", - "roomName": "Romnavn", - "persistentRoom": "Permanent rom", - "persistentRoomHint": "Permanente rom forblir oppført etter at møtet er avsluttet og kan brukes om igjen.", - "noRooms": "Ingen rom ennå. Opprett et for å starte et møte.", - "openRoom": "Åpne", - "endRoom": "Avslutt møtet", - "leaveRoom": "Forlate", - "deleteMember": "Fjerne", - "removeMember": "Fjern {{name}}", - "removeMemberTitle": "Fjerne dette medlemmet?", - "removeMemberDescription": "{{name}} mister tilgangen til dette rommet umiddelbart. Alle aktive SSH-lesere blir frakoblet.", - "invite": "Invitere", - "inviteTitle": "Inviter medlemmer", - "members": "Medlemmer", - "membersPanel": "Rommedlemmer og kontrollforespørsler", - "membersWithCount": "Medlemmer ({{count}})", - "online": "på nett", - "offline": "frakoblet", - "you": "(du)", - "hostBadge": "Vert", - "presenterBadge": "Presenterer", - "present": "Nåværende", - "presentTitle": "Innta scenen", - "presentHost": "Vert", - "searchHosts": "Søk etter verter", - "noHostsFound": "Ingen tilgjengelige verter funnet.", - "presentProtocol": "Protokoll", - "stopPresenting": "Slutt å presentere", - "takeOver": "Ta over", - "takeOverConfirmTitle": "Erstatte den nåværende presentasjonen?", - "takeOverDescription": "{{name}} presenterer nå. Å ta over vil koble alle fra det stadiet.", - "emptyStage": "Ingen presenterer. Gå på scenen for å dele en økt.", - "stageLoading": "Kobler til scenen...", - "stageEnded": "Presentasjonen ble avsluttet", - "roomEnded": "Dette møtet er avsluttet", - "presenterLabel": "{{name}} presenterer", - "guestLink": "Gjestelenke", - "guestLinkOn": "Gjestelenken er på. Alle med lenken kan se scenen.", - "guestLinkOff": "Gjestelenken er av.", - "copyLink": "Kopier lenke", - "rotateLink": "Roter lenken", - "disableLink": "Deaktiver lenken", - "rotateLinkConfirmTitle": "Roter gjestelenken?", - "rotateLinkDescription": "Den nåværende lenken vil slutte å virke umiddelbart. Tilkoblede SSH-gjester vil bli frakoblet.", - "disableLinkConfirmTitle": "Deaktivere gjestelenken?", - "disableLinkDescription": "Anonyme SSH-lesere vil bli frakoblet umiddelbart, og den nåværende koblingen vil slutte å fungere.", - "linkCopied": "Lenken er kopiert", - "roles": "Roller", - "users": "Brukere", - "searchUsers": "Søk etter brukere", - "endConfirmTitle": "Avslutte dette møtet?", - "endDescription": "{{name}} vil avsluttes for alle og kan ikke åpnes igjen.", - "endPersistentDescription": "Den aktive presentasjonen i {{name}} stopper. Rommet er fortsatt tilgjengelig for gjenbruk.", - "deleteRoom": "Slett rom", - "deleteConfirmTitle": "Slette dette rommet?", - "deleteDescription": "{{name}} vil bli permanent slettet for alle og kan ikke angres.", - "invitedTo": "Du ble invitert til «{{name}}»", - "guest": { - "title": "Møte", - "waiting": "Venter på programlederen...", - "linkInvalid": "Denne gjestelenken er ugyldig, eller møtet er avsluttet" - }, - "reopenFromPanel": "Denne møtefanen er utløpt. Åpne rommet på nytt fra Møter-panelet.", - "requestControl": "Be om kontroll", - "cancelControlRequest": "Avbryt forespørsel", - "controlQueue": "Kontrollforespørsler", - "noControlRequests": "Ingen ventende forespørsler.", - "grantControlTo": "Gi kontroll til {{name}}", - "dismissControlRequest": "Avvis kontrollforespørselen til {{name}}", - "releaseControl": "Slipp kontrollen", - "grantControl": "Gi kontroll", - "revokeControl": "Ta tilbake kontrollen", - "controlRequestedBy": "{{name}} ba om kontroll", - "grant": "Stipend", - "controlBadge": "I kontroll", - "youArePresenting": "Du presenterer i en annen fane. Stopp presentasjonen for å gi fra deg scenen.", - "created": "Rommet er opprettet", - "invited": "Medlemmer invitert", - "roomTab": "Møte" - }, "sessionSharing": { - "guestView": { - "loading": "Kobler til delt økt...", - "linkInvalid": "Denne delingslenken er ugyldig, utløpt eller har blitt tilbakekalt", - "rateLimited": "For mange forsøk, prøv igjen snart", - "sessionEnded": "Denne økten er avsluttet", - "readOnlyBadge": "Kun visning", - "ownerLabel": "Vertseier" - }, - "modalTitle": "Del økt", - "shareButton": "Dele", - "notReadyToShare": "Økten er ikke klar til deling ennå", - "modeTab": { - "link": "Lenke", - "user": "Bruker" - }, - "linkModeDescription": "Alle med denne lenken kan bli med, ingen konto kreves.", - "userModeDescription": "Del med en bestemt bruker som allerede har tilgang til denne verten. Hvis de ikke har tilgang ennå, del verten med dem først eller bruk en lenke i stedet. Når økten er delt, vises den i deres Tilkoblinger-fane.", "permissionLevel": { - "label": "Tillatelsesnivå", "readOnly": "Skrivebeskyttet", - "readOnlyDescription": "Kan se økten direkte, men kan ikke skrive eller samhandle.", - "readWrite": "Lese-skrive", - "readWriteDescription": "Kan skrive og samhandle med økten akkurat som eieren." - }, - "expiryLabel": "Utløpsdato for lenke", - "createLinkButton": "Opprett lenke", - "createShareButton": "Del med brukeren", - "searchUsersPlaceholder": "Søk etter brukere...", - "noUsersFound": "Ingen brukere funnet", - "linkCreated": "Delingslenke opprettet", - "linkCopied": "Lenke kopiert til utklippstavlen", - "copyLink": "Kopier lenke", - "shareCreated": "Økten er delt. Den vil vises i Tilkoblinger-fanen deres.", - "shareFailed": "Kunne ikke opprette delingen", - "userLacksHostAccess": "Den brukeren har ikke tilgang til denne verten ennå. Del verten med dem først, eller bruk en lenke i stedet.", - "activeShares": "Aktive aksjer", - "noActiveShares": "Ingen aktive delinger for denne økten", - "revoke": "Tilbakekalling", - "revokeConfirmTitle": "Tilbakekalle denne delingen?", - "revokeConfirmDescription": "Alle som bruker denne delingen vil miste tilgangen umiddelbart.", - "revoked": "Deling tilbakekalt", - "revokeFailed": "Kunne ikke tilbakekalle delingen", - "joinCount": "{{count}} bli med", - "joinCount_other": "{{count}} blir med", - "expiresAt": "Utløper {{date}}", - "linkShareBadge": "Lenke", - "userShareBadge": "Bruker: {{username}}", - "loadSharesFailed": "Kunne ikke laste inn aktive delinger" + "readWrite": "Lese-skrive" + } }, "terminal": { "connect": "Koble til vert", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Sender en anonym daglig telling av brukere, verter og funksjonsbruk for å forbedre Termix. Ingen personopplysninger eller tilkoblingsdetaljer inkluderes.", "analyticsEnabledLockedDesc": "Denne innstillingen er låst av miljøvariabelen ENABLE_TELEMETRY og kan ikke endres her.", "updateAnalyticsFailed": "Kunne ikke oppdatere analyseinnstillingen", - "sessionSharingGloballyEnabled": "Tillat øktdeling", "auditForwardingTitle": "Videresend til SIEM", "auditForwardingDesc": "Send en kopi av hver revisjonsoppføring til en ekstern innsamler som NDJSON. Lokale logger forblir kilden til sannheten.", "auditForwardingEnvNotice": "En miljøvariabel konfigurerer også videresending; URL-en som lagres her har forrang.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Bærertoken (lagret)", "auditForwardingSaved": "Innstillinger for videresending av revisjon lagret", "auditForwardingSaveError": "Kunne ikke lagre innstillingene for videresending av revisjon", - "sessionSharingGloballyEnabledDesc": "Tillat deling av Live Terminal-, RDP-, VNC- og Telnet-økter på tvers av hele instansen. Overstyrer alle delingsalternativer per vert når de er deaktivert.", - "updateSessionSharingFailed": "Kunne ikke oppdatere innstillingen for deling av økt", "sessionTimeout": "Tidsavbrudd for økt", "hours": "timer", "sessionTimeoutRange": "Min. 1 t · Maks. 720 t", diff --git a/src/ui/locales/translated/pl_PL.json b/src/ui/locales/translated/pl_PL.json index 48830d9ce..38248996d 100644 --- a/src/ui/locales/translated/pl_PL.json +++ b/src/ui/locales/translated/pl_PL.json @@ -667,7 +667,6 @@ "done_cli_link": "Otwórz dokumentację CLI" }, "nav": { - "collab": "Spotkania", "home": "Start", "terminal": "Terminal", "localTerminal": "Terminal lokalny", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Utrzymuj działanie powłoki i wszystkiego, co w niej działa, nawet po rozłączeniu i ponownym uruchomieniu przeglądarki, uruchamiając ją wewnątrz tmux (tmux musi być zainstalowany na hoście).", "enableSessionLogging": "Rejestrowanie sesji", "enableSessionLoggingDesc": "Zapisuj wyjście sesji terminala do późniejszego przejrzenia", - "allowSessionSharing": "Zezwól na udostępnianie sesji", - "allowSessionSharingDesc": "Zezwól na udostępnianie sesji na żywo na tym hoście przez link lub innym użytkownikom", "localEchoLabel": "Lokalne echo", "localEchoDesc": "Przewiduj lokalnie bezpieczne, drukowalne znaki na wolnych łączach. Monity o hasło i znaki sterujące nigdy nie są powtarzane.", "localEchoDefault": "Użyj ustawienia globalnego", @@ -1571,137 +1568,11 @@ "join": "Dołącz", "sharedSessionLabel": "{{hostName}} (udostępnione)" }, - "collab": { - "createRoom": "Nowy pokój", - "roomName": "Nazwa pokoju", - "persistentRoom": "Trwały pokój", - "persistentRoomHint": "Trwałe pokoje pozostają na liście po zakończeniu spotkania i można je ponownie wykorzystać.", - "noRooms": "Brak pokoi. Utwórz jeden, aby rozpocząć spotkanie.", - "openRoom": "Otwarte", - "endRoom": "Zakończ spotkanie", - "leaveRoom": "Wyjechać", - "deleteMember": "Usunąć", - "removeMember": "Usuń {{name}}", - "removeMemberTitle": "Usunąć tego członka?", - "removeMemberDescription": "{{name}} natychmiast utraci dostęp do tego pokoju. Każdy aktywny użytkownik SSH zostanie rozłączony.", - "invite": "Zapraszać", - "inviteTitle": "Zaproś członków", - "members": "Członkowie", - "membersPanel": "Członkowie pokoju i prośby o kontrolę", - "membersWithCount": "Członkowie ({{count}})", - "online": "w sieci", - "offline": "nieaktywny", - "you": "(Ty)", - "hostBadge": "Gospodarz", - "presenterBadge": "Przedstawianie", - "present": "Obecny", - "presentTitle": "Wejdź na scenę", - "presentHost": "Gospodarz", - "searchHosts": "Wyszukaj gospodarzy", - "noHostsFound": "Nie znaleziono dostępnych hostów.", - "presentProtocol": "Protokół", - "stopPresenting": "Zatrzymaj prezentację", - "takeOver": "Przejąć", - "takeOverConfirmTitle": "Zastąpić bieżącą prezentację?", - "takeOverDescription": "{{name}} jest teraz prezentowany. Przejęcie kontroli spowoduje odłączenie wszystkich od tej sceny.", - "emptyStage": "Nikt nie prowadzi prezentacji. Wejdź na scenę i podziel się swoją sesją.", - "stageLoading": "Łączę się ze sceną...", - "stageEnded": "Prezentacja dobiegła końca", - "roomEnded": "To spotkanie dobiegło końca", - "presenterLabel": "{{name}} prezentuje", - "guestLink": "Link gościnny", - "guestLinkOn": "Link dla gości jest włączony. Każdy, kto ma link, może obejrzeć scenę.", - "guestLinkOff": "Link dla gości jest wyłączony.", - "copyLink": "Kopiuj link", - "rotateLink": "Obróć link", - "disableLink": "Wyłącz łącze", - "rotateLinkConfirmTitle": "Obrócić link gościa?", - "rotateLinkDescription": "Obecny link natychmiast przestanie działać. Połączeni goście SSH zostaną rozłączeni.", - "disableLinkConfirmTitle": "Wyłączyć link gościa?", - "disableLinkDescription": "Anonimowi użytkownicy SSH zostaną natychmiast rozłączeni, a bieżące łącze przestanie działać.", - "linkCopied": "Link skopiowany", - "roles": "Role", - "users": "Użytkownicy", - "searchUsers": "Wyszukaj użytkowników", - "endConfirmTitle": "Zakończyć to spotkanie?", - "endDescription": "{{name}} zakończy się dla wszystkich i nie będzie można go ponownie otworzyć.", - "endPersistentDescription": "Aktywna prezentacja w {{name}} zostanie zatrzymana. Pokój pozostaje dostępny do ponownego użycia.", - "deleteRoom": "Usuń pokój", - "deleteConfirmTitle": "Usunąć ten pokój?", - "deleteDescription": "{{name}} zostanie trwale usunięty u wszystkich i nie będzie można tego cofnąć.", - "invitedTo": "Zostałeś zaproszony do „{{name}}”", - "guest": { - "title": "Spotkanie", - "waiting": "Czekam na prezentera...", - "linkInvalid": "Ten link dla gościa jest nieprawidłowy lub spotkanie się zakończyło" - }, - "reopenFromPanel": "Ta karta spotkania wygasła. Otwórz ponownie pokój z panelu Spotkania.", - "requestControl": "Żądanie kontroli", - "cancelControlRequest": "Anuluj prośbę", - "controlQueue": "Żądania kontrolne", - "noControlRequests": "Brak oczekujących żądań.", - "grantControlTo": "Przekaż kontrolę {{name}}", - "dismissControlRequest": "Odrzuć żądanie kontroli {{name}}", - "releaseControl": "Kontrola wydania", - "grantControl": "Oddaj kontrolę", - "revokeControl": "Odzyskaj kontrolę", - "controlRequestedBy": "{{name}} poprosił o kontrolę", - "grant": "Dotacja", - "controlBadge": "Pod kontrolą", - "youArePresenting": "Prezentujesz na innej karcie. Zatrzymaj prezentację, aby przekazać scenę.", - "created": "Pokój utworzony", - "invited": "Zaproszeni członkowie", - "roomTab": "Spotkanie" - }, "sessionSharing": { - "guestView": { - "loading": "Łączenie z sesją współdzieloną...", - "linkInvalid": "Ten link do udostępniania jest nieprawidłowy, wygasł lub został odwołany", - "rateLimited": "Zbyt wiele prób, spróbuj ponownie za chwilę", - "sessionEnded": "Ta sesja dobiegła końca", - "readOnlyBadge": "Tylko do podglądu", - "ownerLabel": "Właściciel gospodarza" - }, - "modalTitle": "Udostępnij sesję", - "shareButton": "Udostępnij", - "notReadyToShare": "Sesja nie jest jeszcze gotowa do udostępnienia", - "modeTab": { - "link": "Link", - "user": "Użytkownik" - }, - "linkModeDescription": "Każdy mający ten link może dołączyć, nie jest wymagane założenie konta.", - "userModeDescription": "Udostępnij konkretnemu użytkownikowi, który ma już dostęp do tego hosta. Jeśli jeszcze go nie ma, najpierw udostępnij mu hosta albo skorzystaj z linku. Po udostępnieniu sesja pojawi się na jego karcie Połączenia.", "permissionLevel": { - "label": "Poziom uprawnień", "readOnly": "Tylko do odczytu", - "readOnlyDescription": "Może oglądać sesję na żywo, ale nie może pisać ani nic w niej zmieniać.", - "readWrite": "Odczyt-zapis", - "readWriteDescription": "Może pisać i uczestniczyć w sesji tak samo jak właściciel." - }, - "expiryLabel": "Wygaśnięcie linku", - "createLinkButton": "Utwórz link", - "createShareButton": "Udostępnij użytkownikowi", - "searchUsersPlaceholder": "Wyszukaj użytkowników...", - "noUsersFound": "Nie znaleziono użytkowników", - "linkCreated": "Utworzono link do udostępniania", - "linkCopied": "Link skopiowany do schowka", - "copyLink": "Kopiuj link", - "shareCreated": "Udostępniono sesję. Pojawi się na jego karcie Połączenia.", - "shareFailed": "Nie udało się utworzyć udostępnienia", - "userLacksHostAccess": "Ten użytkownik nie ma jeszcze dostępu do tego hosta. Udostępnij mu najpierw hosta lub skorzystaj z linku.", - "activeShares": "Aktywne udostępnienia", - "noActiveShares": "Brak aktywnych udostępnień tej sesji", - "revoke": "Unieważnij", - "revokeConfirmTitle": "Cofnąć to udostępnienie?", - "revokeConfirmDescription": "Każdy, kto korzysta z tego udostępnienia, natychmiast utraci dostęp.", - "revoked": "Unieważniono udostępnienie", - "revokeFailed": "Nie udało się unieważnić udostępnienia", - "joinCount": "{{count}} dołączenie", - "joinCount_other": "{{count}} dołączeń", - "expiresAt": "Wygasa {{date}}", - "linkShareBadge": "Link", - "userShareBadge": "Użytkownik: {{username}}", - "loadSharesFailed": "Nie udało się załadować aktywnych udostępnień" + "readWrite": "Odczyt-zapis" + } }, "terminal": { "connect": "Połącz z hostem", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Wysyła anonimowe, dzienne zestawienie liczby użytkowników, hostów i wykorzystania funkcji, aby pomóc w ulepszaniu Termixa. Nigdy nie zawiera danych osobowych ani szczegółów połączeń.", "analyticsEnabledLockedDesc": "To ustawienie jest zablokowane przez zmienną środowiskową ENABLE_TELEMETRY i nie można go tutaj zmienić.", "updateAnalyticsFailed": "Nie udało się zaktualizować ustawień analityki", - "sessionSharingGloballyEnabled": "Zezwól na udostępnianie sesji", "auditForwardingTitle": "Przekaż do SIEM", "auditForwardingDesc": "Prześlij kopię każdego wpisu audytu do zewnętrznego kolektora w formacie NDJSON. Lokalne logi pozostają źródłem prawdy.", "auditForwardingEnvNotice": "Zmienna środowiskowa konfiguruje również przekazywanie; pierwszeństwo ma zapisany tutaj adres URL.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Token okaziciela (przechowywany)", "auditForwardingSaved": "Ustawienia przekazywania audytu zostały zapisane", "auditForwardingSaveError": "Nie udało się zapisać ustawień przekazywania audytu", - "sessionSharingGloballyEnabledDesc": "Zezwalaj na udostępnianie sesji terminala na żywo, RDP, VNC i Telnet w obrębie instancji. Po wyłączeniu zastępuje wszystkie przełączniki udostępniania dla poszczególnych hostów.", - "updateSessionSharingFailed": "Nie udało się zaktualizować ustawień udostępniania sesji", "sessionTimeout": "Limit czasu sesji", "hours": "godziny", "sessionTimeoutRange": "Min. 1 godz. · Maks. 720 godz.", diff --git a/src/ui/locales/translated/pt_BR.json b/src/ui/locales/translated/pt_BR.json index aa24357e6..56d6355c7 100644 --- a/src/ui/locales/translated/pt_BR.json +++ b/src/ui/locales/translated/pt_BR.json @@ -667,7 +667,6 @@ "done_cli_link": "Abra a documentação da CLI." }, "nav": { - "collab": "Reuniões", "home": "Lar", "terminal": "terminal", "localTerminal": "Terminal local", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Mantenha seu shell e tudo o que estiver sendo executado nele em funcionamento mesmo após desconexões e reinicializações do navegador, executando o comando dentro do tmux (o tmux precisa estar instalado no host).", "enableSessionLogging": "Registro de sessão", "enableSessionLoggingDesc": "Registre a saída da sessão do terminal para revisão posterior.", - "allowSessionSharing": "Permitir compartilhamento de sessão", - "allowSessionSharingDesc": "Permitir que as sessões ao vivo neste servidor sejam compartilhadas por meio de link ou com outros usuários.", "localEchoLabel": "Eco Local", "localEchoDesc": "Preveja sequências de teclas seguras e imprimíveis localmente em conexões lentas. Os prompts de senha e as entradas de controle nunca são repetidos.", "localEchoDefault": "Usar configuração global", @@ -1571,137 +1568,11 @@ "join": "Juntar", "sharedSessionLabel": "{{hostName}} (compartilhado)" }, - "collab": { - "createRoom": "Quarto novo", - "roomName": "Nome do quarto", - "persistentRoom": "Sala persistente", - "persistentRoomHint": "As salas permanentes permanecem listadas após o término da reunião e podem ser reutilizadas.", - "noRooms": "Ainda não há salas. Crie uma para iniciar uma reunião.", - "openRoom": "Abrir", - "endRoom": "Encerramento da reunião", - "leaveRoom": "Deixar", - "deleteMember": "Remover", - "removeMember": "Remover {{name}}", - "removeMemberTitle": "Remover este membro?", - "removeMemberDescription": "{{name}} perderá o acesso a esta sala imediatamente. Qualquer visualizador SSH ativo será desconectado.", - "invite": "Convidar", - "inviteTitle": "Convide membros", - "members": "Membros", - "membersPanel": "Membros da sala e solicitações de controle", - "membersWithCount": "Membros ({{count}})", - "online": "on-line", - "offline": "offline", - "you": "(você)", - "hostBadge": "Hospedar", - "presenterBadge": "Apresentando", - "present": "Presente", - "presentTitle": "Suba ao palco", - "presentHost": "Hospedar", - "searchHosts": "Pesquisar anfitriões", - "noHostsFound": "Nenhum host disponível encontrado.", - "presentProtocol": "Protocolo", - "stopPresenting": "Pare de apresentar", - "takeOver": "Assumir", - "takeOverConfirmTitle": "Substituir a apresentação atual?", - "takeOverDescription": "{{name}} está apresentando agora. Assumir o controle desconectará todos desse palco.", - "emptyStage": "Ninguém está apresentando. Suba ao palco para compartilhar uma sessão.", - "stageLoading": "Conectando-se ao palco...", - "stageEnded": "A apresentação terminou.", - "roomEnded": "Esta reunião terminou.", - "presenterLabel": "{{name}} está apresentando", - "guestLink": "Link para convidado", - "guestLinkOn": "O link para convidados está ativado. Qualquer pessoa com o link pode assistir à apresentação.", - "guestLinkOff": "O link para visitantes está desativado.", - "copyLink": "Copiar link", - "rotateLink": "Rotacionar link", - "disableLink": "Desativar link", - "rotateLinkConfirmTitle": "Girar o link do convidado?", - "rotateLinkDescription": "A conexão atual deixará de funcionar imediatamente. Os dispositivos convidados SSH conectados serão desconectados.", - "disableLinkConfirmTitle": "Desativar o link de convidado?", - "disableLinkDescription": "Os visualizadores SSH anônimos serão desconectados imediatamente e a conexão atual deixará de funcionar.", - "linkCopied": "Link copiado", - "roles": "Funções", - "users": "Usuários", - "searchUsers": "Usuários da pesquisa", - "endConfirmTitle": "Encerrar esta reunião?", - "endDescription": "{{name}} será encerrado para todos e não poderá ser reaberto.", - "endPersistentDescription": "A apresentação ativa em {{name}} será interrompida. A sala permanece disponível para reutilização.", - "deleteRoom": "Excluir sala", - "deleteConfirmTitle": "Apagar esta sala?", - "deleteDescription": "{{name}} será excluído permanentemente para todos e não poderá ser desfeito.", - "invitedTo": "Você foi convidado para \"{{name}}\"", - "guest": { - "title": "Reunião", - "waiting": "Aguardando o apresentador...", - "linkInvalid": "Este link de convidado é inválido ou a reunião já terminou." - }, - "reopenFromPanel": "Esta aba da reunião expirou. Reabra a sala no painel Reuniões.", - "requestControl": "Controle de solicitação", - "cancelControlRequest": "Cancelar solicitação", - "controlQueue": "Solicitações de controle", - "noControlRequests": "Não há solicitações pendentes.", - "grantControlTo": "Dê o controle para {{name}}", - "dismissControlRequest": "Rejeitar solicitação de controle de {{name}}", - "releaseControl": "Liberar controle", - "grantControl": "Dê o controle", - "revokeControl": "Retome o controle", - "controlRequestedBy": "{{name}} solicitou o controle", - "grant": "Conceder", - "controlBadge": "No controle", - "youArePresenting": "Você está apresentando em outra aba. Pare de apresentar para passar a palavra.", - "created": "Quarto criado", - "invited": "Membros convidados", - "roomTab": "Reunião" - }, "sessionSharing": { - "guestView": { - "loading": "Conectando à sessão compartilhada...", - "linkInvalid": "Este link de compartilhamento é inválido, expirou ou foi revogado.", - "rateLimited": "Muitas tentativas foram feitas, tente novamente em breve.", - "sessionEnded": "Esta sessão terminou.", - "readOnlyBadge": "Somente visualização", - "ownerLabel": "Proprietário do anfitrião" - }, - "modalTitle": "Sessão de compartilhamento", - "shareButton": "Compartilhar", - "notReadyToShare": "A sessão ainda não está pronta para ser compartilhada.", - "modeTab": { - "link": "Link", - "user": "Usuário" - }, - "linkModeDescription": "Qualquer pessoa com este link pode participar, sem necessidade de criar uma conta.", - "userModeDescription": "Compartilhe com um usuário específico que já tenha acesso a este host. Caso ele ainda não tenha acesso, compartilhe o host com ele primeiro ou use um link. Após o compartilhamento, a sessão aparecerá na aba Conexões do usuário.", "permissionLevel": { - "label": "Nível de permissão", "readOnly": "Somente leitura", - "readOnlyDescription": "É possível assistir à sessão ao vivo, mas não é possível digitar ou interagir.", - "readWrite": "Ler e escrever", - "readWriteDescription": "Pode digitar e interagir com a sessão da mesma forma que o proprietário." - }, - "expiryLabel": "Expiração do link", - "createLinkButton": "Criar link", - "createShareButton": "Compartilhar com o usuário", - "searchUsersPlaceholder": "Pesquisar usuários...", - "noUsersFound": "Nenhum usuário encontrado", - "linkCreated": "Compartilhar link criado", - "linkCopied": "Link copiado para a área de transferência", - "copyLink": "Copiar link", - "shareCreated": "Sessão compartilhada. Ela aparecerá na aba Conexões deles.", - "shareFailed": "Falha ao criar compartilhamento", - "userLacksHostAccess": "Esse usuário ainda não tem acesso a este host. Compartilhe o host com ele primeiro ou use um link.", - "activeShares": "Ações ativas", - "noActiveShares": "Nenhuma ação ativa nesta sessão.", - "revoke": "Revogar", - "revokeConfirmTitle": "Revogar essa participação?", - "revokeConfirmDescription": "Qualquer pessoa que usar este compartilhamento perderá o acesso imediatamente.", - "revoked": "Ações revogadas", - "revokeFailed": "Falha ao revogar a participação", - "joinCount": "{{count}} junte-se", - "joinCount_other": "{{count}} entra", - "expiresAt": "Expira em {{date}}", - "linkShareBadge": "Link", - "userShareBadge": "Usuário: {{username}}", - "loadSharesFailed": "Falha ao carregar compartilhamentos ativos" + "readWrite": "Ler e escrever" + } }, "terminal": { "connect": "Conectar ao host", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Envia uma contagem diária anônima de usuários, hosts e uso de recursos para ajudar a melhorar o Termix. Nenhum dado pessoal ou detalhe de conexão é incluído.", "analyticsEnabledLockedDesc": "Essa configuração está bloqueada pela variável de ambiente ENABLE_TELEMETRY e não pode ser alterada aqui.", "updateAnalyticsFailed": "Falha ao atualizar as configurações de análise", - "sessionSharingGloballyEnabled": "Permitir compartilhamento de sessão", "auditForwardingTitle": "Encaminhar para SIEM", "auditForwardingDesc": "Envie uma cópia de cada entrada de auditoria para um coletor externo no formato NDJSON. Os registros locais permanecem como a fonte da verdade.", "auditForwardingEnvNotice": "Uma variável de ambiente também configura o encaminhamento; a URL salva aqui tem precedência.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Token de portador (armazenado)", "auditForwardingSaved": "Configurações de encaminhamento de auditoria salvas", "auditForwardingSaveError": "Falha ao salvar as configurações de encaminhamento de auditoria", - "sessionSharingGloballyEnabledDesc": "Permite que sessões de terminal ao vivo, RDP, VNC e Telnet sejam compartilhadas em toda a instância. Substitui todas as configurações de compartilhamento por host quando desativada.", - "updateSessionSharingFailed": "Falha ao atualizar as configurações de compartilhamento de sessão.", "sessionTimeout": "Tempo limite da sessão", "hours": "horas", "sessionTimeoutRange": "Mínimo 1h · Máximo 720h", diff --git a/src/ui/locales/translated/pt_PT.json b/src/ui/locales/translated/pt_PT.json index 668e0ad1f..537bfd257 100644 --- a/src/ui/locales/translated/pt_PT.json +++ b/src/ui/locales/translated/pt_PT.json @@ -667,7 +667,6 @@ "done_cli_link": "Abra a documentação da CLI." }, "nav": { - "collab": "Reuniões", "home": "Início", "terminal": "Terminal", "localTerminal": "Terminal local", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Mantenha o seu shell e tudo o que nele está a ser executado em funcionamento mesmo após desconexões e reinicializações do browser, executando o comando dentro do tmux (o tmux necessita de estar instalado no host).", "enableSessionLogging": "Registo de sessão", "enableSessionLoggingDesc": "Gravar a saída da sessão de terminal para revisão posterior", - "allowSessionSharing": "Permitir partilha de sessão", - "allowSessionSharingDesc": "Permitir que as sessões em direto neste servidor sejam partilhadas através de link ou com outros utilizadores.", "localEchoLabel": "Eco Local", "localEchoDesc": "Preveja sequências de teclas seguras e imprimíveis localmente em ligações lentas. Os avisos de palavra-passe e as entradas de controlo nunca são repetidos.", "localEchoDefault": "Usar configuração global", @@ -1571,137 +1568,11 @@ "join": "Juntar", "sharedSessionLabel": "{{hostName}} (partilhado)" }, - "collab": { - "createRoom": "Quarto novo", - "roomName": "Nome do quarto", - "persistentRoom": "Sala persistente", - "persistentRoomHint": "As salas permanentes permanecem listadas após o término da reunião e podem ser reutilizadas.", - "noRooms": "Ainda não há salas. Crie uma para iniciar uma reunião.", - "openRoom": "Aberto", - "endRoom": "Encerramento da reunião", - "leaveRoom": "Ausência", - "deleteMember": "Remover", - "removeMember": "Remover {{name}}", - "removeMemberTitle": "Remover este membro?", - "removeMemberDescription": "{{name}} perderá o acesso a esta sala imediatamente. Qualquer visualizador SSH ativo será desligado.", - "invite": "Convidar", - "inviteTitle": "Convide os membros", - "members": "Membros", - "membersPanel": "Membros da sala e pedidos de controlo", - "membersWithCount": "Membros ({{count}})", - "online": "online", - "offline": "offline", - "you": "(tu)", - "hostBadge": "Anfitrião", - "presenterBadge": "Apresentando", - "present": "Presente", - "presentTitle": "Suba ao palco", - "presentHost": "Anfitrião", - "searchHosts": "Pesquisar anfitriões", - "noHostsFound": "Nenhum host disponível encontrado.", - "presentProtocol": "Protocolo", - "stopPresenting": "Pare de apresentar", - "takeOver": "Assumir", - "takeOverConfirmTitle": "Substituir a apresentação atual?", - "takeOverDescription": "{{name}} está a apresentar agora. Assumir o controlo desconectará todos desse palco.", - "emptyStage": "Ninguém está a apresentar. Suba ao palco para partilhar uma sessão.", - "stageLoading": "Ligar-se ao palco...", - "stageEnded": "A apresentação terminou.", - "roomEnded": "Esta reunião terminou.", - "presenterLabel": "{{name}} está a apresentar", - "guestLink": "Link para convidado", - "guestLinkOn": "O link para convidados está ativado. Qualquer pessoa com o link pode assistir à apresentação.", - "guestLinkOff": "O link para os visitantes está desativado.", - "copyLink": "Copiar link", - "rotateLink": "Rotacionar ligação", - "disableLink": "Desativar ligação", - "rotateLinkConfirmTitle": "Rodar o link do convidado?", - "rotateLinkDescription": "A ligação atual deixará de funcionar imediatamente. Os dispositivos convidados SSH ligados serão desligados.", - "disableLinkConfirmTitle": "Desativar o link de convidado?", - "disableLinkDescription": "Os visualizadores SSH anónimos serão imediatamente desligados e a ligação atual deixará de funcionar.", - "linkCopied": "Link copiado", - "roles": "Funções", - "users": "Usuários", - "searchUsers": "Os utilizadores da pesquisa", - "endConfirmTitle": "Encerrar esta reunião?", - "endDescription": "{{name}} será encerrado para todos e não poderá ser reaberto.", - "endPersistentDescription": "A apresentação ativa em {{name}} será interrompida. A sala continua disponível para reutilização.", - "deleteRoom": "Apagar sala", - "deleteConfirmTitle": "Apagar esta sala?", - "deleteDescription": "{{name}} será eliminado permanentemente para todos e não poderá ser anulado.", - "invitedTo": "Foi convidado para \"{{name}}\"", - "guest": { - "title": "Reunião", - "waiting": "À espera do apresentador...", - "linkInvalid": "Este link de convidado é inválido ou a reunião já terminou." - }, - "reopenFromPanel": "Esta aba da reunião expirou. Volte a abrir a sala no painel Reuniões.", - "requestControl": "Controle de solicitação", - "cancelControlRequest": "Cancelar pedido", - "controlQueue": "Solicitações de controlo", - "noControlRequests": "Não há pedidos pendentes.", - "grantControlTo": "Dê o controlo a {{name}}", - "dismissControlRequest": "Rejeitar pedido de controlo de {{name}}", - "releaseControl": "Libertar controle", - "grantControl": "Dê o controlo", - "revokeControl": "Retome o comando", - "controlRequestedBy": "{{name}} solicitou o controlo", - "grant": "Subvenção", - "controlBadge": "No controlo", - "youArePresenting": "Está a apresentar em outra aba. Pare de apresentar para passar a palavra.", - "created": "Quarto criado", - "invited": "Membros convidados", - "roomTab": "Reunião" - }, "sessionSharing": { - "guestView": { - "loading": "Ligar à sessão partilhada...", - "linkInvalid": "Este link de partilha é inválido, expirou ou foi revogado.", - "rateLimited": "Muitas tentativas foram feitas, tente novamente em breve.", - "sessionEnded": "Esta sessão terminou.", - "readOnlyBadge": "Somente visualização", - "ownerLabel": "Proprietário do anfitrião" - }, - "modalTitle": "Sessão de partilha", - "shareButton": "Partilhar", - "notReadyToShare": "A sessão ainda não está pronta para ser partilhada.", - "modeTab": { - "link": "Ligação", - "user": "Usuário" - }, - "linkModeDescription": "Qualquer pessoa com este link pode participar, sem necessidade de criar conta.", - "userModeDescription": "Partilhe com um utilizador específico que já tenha acesso a este host. Caso ainda não tenha acesso, partilhe primeiro o host com ele ou utilize um link. Após a partilha, a sessão aparecerá no separador Conexões do utilizador.", "permissionLevel": { - "label": "Nível de permissão", "readOnly": "Somente leitura", - "readOnlyDescription": "É possível assistir à sessão em direto, mas não é possível digitar ou interagir.", - "readWrite": "Ler e escrever", - "readWriteDescription": "Pode digitar e interagir com a sessão da mesma forma que o proprietário." - }, - "expiryLabel": "Expiração do link", - "createLinkButton": "Criar link", - "createShareButton": "Partilhar com o usuário", - "searchUsersPlaceholder": "Pesquisar utilizadores...", - "noUsersFound": "Nenhum utilizador encontrado", - "linkCreated": "Partilhar link criado", - "linkCopied": "Ligação copiada para a área de transferência", - "copyLink": "Copiar link", - "shareCreated": "Sessão partilhada. Ela aparecerá no separador Conexões deles.", - "shareFailed": "Falha ao criar partilha", - "userLacksHostAccess": "Esse utilizador ainda não tem acesso a este host. Partilhe o host com ele primeiro ou use um link.", - "activeShares": "Ações ativas", - "noActiveShares": "Sem ação ativa nesta sessão.", - "revoke": "Revogar", - "revokeConfirmTitle": "Revogar essa participação?", - "revokeConfirmDescription": "Qualquer pessoa que utilize esta partilha perderá o acesso imediatamente.", - "revoked": "Ações revogadas", - "revokeFailed": "Falha ao revogar a participação", - "joinCount": "{{count}} junte-se", - "joinCount_other": "{{count}} entra", - "expiresAt": "Expira em {{date}}", - "linkShareBadge": "Ligação", - "userShareBadge": "Utilizador: {{username}}", - "loadSharesFailed": "Falha ao carregar partilhas ativas" + "readWrite": "Ler e escrever" + } }, "terminal": { "connect": "Ligar ao Host", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Envia uma contagem diária anónima de utilizadores, hosts e utilização de recursos para ajudar a melhorar o Termix. Nenhum dado pessoal ou detalhe de ligação é incluído.", "analyticsEnabledLockedDesc": "Esta definição está bloqueada pela variável de ambiente ENABLE_TELEMETRY e não pode ser alterada aqui.", "updateAnalyticsFailed": "Falha ao atualizar as definições de análise", - "sessionSharingGloballyEnabled": "Permitir partilha de sessão", "auditForwardingTitle": "Encaminhar para SIEM", "auditForwardingDesc": "Envie uma cópia de cada entrada de auditoria para um coletor externo no formato NDJSON. Os registos locais permanecem como a fonte da verdade.", "auditForwardingEnvNotice": "Uma variável de ambiente também configura o encaminhamento; o URL aqui guardado tem precedência.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Token de portador (armazenado)", "auditForwardingSaved": "Configurações de encaminhamento de auditoria guardadas", "auditForwardingSaveError": "Falha ao guardar as definições de encaminhamento de auditoria", - "sessionSharingGloballyEnabledDesc": "Permite que as sessões de terminal ao vivo, RDP, VNC e Telnet sejam partilhadas em toda a instância. Substitui todas as definições de partilha por host quando desativada.", - "updateSessionSharingFailed": "Falha ao atualizar as definições de partilha de sessão.", "sessionTimeout": "Tempo Limite da Sessão", "hours": "horas", "sessionTimeoutRange": "Mín. 1h · Máx. 720h", diff --git a/src/ui/locales/translated/ro_RO.json b/src/ui/locales/translated/ro_RO.json index f89da6165..265f0a98d 100644 --- a/src/ui/locales/translated/ro_RO.json +++ b/src/ui/locales/translated/ro_RO.json @@ -667,7 +667,6 @@ "done_cli_link": "Deschideți documentația CLI" }, "nav": { - "collab": "Întâlniri", "home": "Acasă", "terminal": "Terminal", "localTerminal": "Terminal local", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Mențineți shell-ul și tot ce rulează în el în funcțiune după deconectări și reporniri ale browserului, rulând în interiorul tmux (tmux trebuie să fie instalat pe gazdă).", "enableSessionLogging": "Înregistrarea sesiunilor", "enableSessionLoggingDesc": "Înregistrați ieșirea sesiunii terminale pentru o revizuire ulterioară", - "allowSessionSharing": "Permiteți partajarea sesiunii", - "allowSessionSharingDesc": "Permiteți partajarea sesiunilor live de pe această gazdă prin link sau cu alți utilizatori", "localEchoLabel": "Ecou local", "localEchoDesc": "Preziceți local apăsările de taste imprimabile în siguranță pe conexiuni lente. Solicitările de parolă și intrările de control nu sunt niciodată reluate.", "localEchoDefault": "Folosește setarea globală", @@ -1571,137 +1568,11 @@ "join": "Alătură-te", "sharedSessionLabel": "{{hostName}} (partajat)" }, - "collab": { - "createRoom": "Cameră nouă", - "roomName": "Numele camerei", - "persistentRoom": "Cameră persistentă", - "persistentRoomHint": "Sălile persistente rămân listate după încheierea întâlnirii și pot fi reutilizate.", - "noRooms": "Încă nu există săli. Creează una pentru a începe o întâlnire.", - "openRoom": "Deschide", - "endRoom": "Încheiați întâlnirea", - "leaveRoom": "Pleacă", - "deleteMember": "Elimina", - "removeMember": "Eliminați {{name}}", - "removeMemberTitle": "Eliminați acest membru?", - "removeMemberDescription": "{{name}} va pierde imediat accesul la această cameră. Orice vizualizator SSH activ va fi deconectat.", - "invite": "Invita", - "inviteTitle": "Invită membri", - "members": "Membri", - "membersPanel": "Membrii camerei și solicitările de control", - "membersWithCount": "Membri ({{count}})", - "online": "online", - "offline": "offline", - "you": "(tu)", - "hostBadge": "Gazdă", - "presenterBadge": "Prezentare", - "present": "Prezent", - "presentTitle": "Urcă pe scenă", - "presentHost": "Gazdă", - "searchHosts": "Căutați gazde", - "noHostsFound": "Nu au fost găsite gazde disponibile.", - "presentProtocol": "Protocol", - "stopPresenting": "Opriți prezentarea", - "takeOver": "Prelua", - "takeOverConfirmTitle": "Înlocuiți prezentarea curentă?", - "takeOverDescription": "{{name}} prezintă acum. Preluarea controlului îi va deconecta pe toți de la acea etapă.", - "emptyStage": "Nimeni nu prezintă. Urcă pe scenă pentru a împărtăși o sesiune.", - "stageLoading": "Conectarea la scenă...", - "stageEnded": "Prezentarea s-a încheiat", - "roomEnded": "Această întâlnire s-a încheiat", - "presenterLabel": "{{name}} prezintă", - "guestLink": "Link pentru invitați", - "guestLinkOn": "Linkul pentru invitați este activat. Oricine are linkul poate viziona scena.", - "guestLinkOff": "Linkul pentru invitați este dezactivat.", - "copyLink": "Copiază linkul", - "rotateLink": "Rotiți linkul", - "disableLink": "Dezactivați linkul", - "rotateLinkConfirmTitle": "Rotește linkul pentru oaspeți?", - "rotateLinkDescription": "Legătura curentă va înceta să funcționeze imediat. Oaspeții SSH conectați vor fi deconectați.", - "disableLinkConfirmTitle": "Dezactivați linkul pentru invitați?", - "disableLinkDescription": "Vizualizatorii SSH anonimi vor fi deconectați imediat, iar legătura curentă va înceta să funcționeze.", - "linkCopied": "Linkul a fost copiat", - "roles": "Roluri", - "users": "Utilizatori", - "searchUsers": "Căutați utilizatori", - "endConfirmTitle": "Încheiați această întâlnire?", - "endDescription": "{{name}} se va încheia pentru toată lumea și nu va putea fi redeschis.", - "endPersistentDescription": "Prezentarea activă din {{name}} se va opri. Sala rămâne disponibilă pentru reutilizare.", - "deleteRoom": "Ștergeți camera", - "deleteConfirmTitle": "Ștergeți această cameră?", - "deleteDescription": "{{name}} va fi șters definitiv pentru toată lumea și nu poate fi anulat.", - "invitedTo": "Ai fost invitat la „{{name}}”", - "guest": { - "title": "Întâlnire", - "waiting": "În așteptarea prezentatorului...", - "linkInvalid": "Acest link de invitat este invalid sau întâlnirea s-a încheiat." - }, - "reopenFromPanel": "Această filă de întâlnire a expirat. Redeschideți sala din panoul Întâlniri.", - "requestControl": "Cerere de control", - "cancelControlRequest": "Anulează cererea", - "controlQueue": "Cereri de control", - "noControlRequests": "Nicio solicitare în așteptare.", - "grantControlTo": "Acordă controlul către {{name}}", - "dismissControlRequest": "Închideți solicitarea de control a utilizatorului {{name}}", - "releaseControl": "Controlul eliberării", - "grantControl": "Oferă controlul", - "revokeControl": "Preia controlul", - "controlRequestedBy": "{{name}} a cerut controlul", - "grant": "Acorda", - "controlBadge": "În control", - "youArePresenting": "Prezentați într-o altă filă. Oprește prezentarea pentru a preda scena.", - "created": "Cameră creată", - "invited": "Membrii invitați", - "roomTab": "Întâlnire" - }, "sessionSharing": { - "guestView": { - "loading": "Conectare la sesiunea partajată...", - "linkInvalid": "Acest link de partajare este invalid, a expirat sau a fost revocat.", - "rateLimited": "Prea multe încercări, vă rugăm să încercați din nou în scurt timp", - "sessionEnded": "Această sesiune s-a încheiat", - "readOnlyBadge": "Doar vizualizare", - "ownerLabel": "Proprietarul gazdei" - }, - "modalTitle": "Partajați sesiunea", - "shareButton": "Distribuie", - "notReadyToShare": "Sesiunea nu este încă gata de partajare", - "modeTab": { - "link": "Legătură", - "user": "Utilizator" - }, - "linkModeDescription": "Oricine are acest link se poate alătura, nu este necesar un cont.", - "userModeDescription": "Partajați cu un anumit utilizator care are deja acces la această gazdă. Dacă nu are încă acces, partajați mai întâi gazda cu acesta sau utilizați un link în schimb. După partajare, sesiunea apare în fila Conexiuni.", "permissionLevel": { - "label": "Nivel de permisiune", "readOnly": "Doar citire", - "readOnlyDescription": "Pot urmări sesiunea în direct, dar nu pot tasta sau interacționa.", - "readWrite": "Citire-scriere", - "readWriteDescription": "Poate tasta și interacționa cu sesiunea la fel ca proprietarul." - }, - "expiryLabel": "Expirarea linkului", - "createLinkButton": "Creați un link", - "createShareButton": "Partajează cu utilizatorul", - "searchUsersPlaceholder": "Căutați utilizatori...", - "noUsersFound": "Nu au fost găsiți utilizatori", - "linkCreated": "Link de partajare creat", - "linkCopied": "Linkul a fost copiat în clipboard", - "copyLink": "Copiază linkul", - "shareCreated": "Sesiune partajată. Va apărea în fila Conexiuni.", - "shareFailed": "Nu s-a putut crea partajarea", - "userLacksHostAccess": "Utilizatorul respectiv nu are încă acces la această gazdă. Partajați mai întâi gazda cu acesta sau folosiți un link în schimb.", - "activeShares": "Acțiuni active", - "noActiveShares": "Nicio partajare activă pentru această sesiune", - "revoke": "Revoca", - "revokeConfirmTitle": "Revocați această partajare?", - "revokeConfirmDescription": "Oricine folosește această partajare va pierde accesul imediat.", - "revoked": "Acțiune revocată", - "revokeFailed": "Nu s-a putut revoca partajarea", - "joinCount": "{{count}} alătură-te", - "joinCount_other": "{{count}} se alătură", - "expiresAt": "Expiră la {{date}}", - "linkShareBadge": "Legătură", - "userShareBadge": "Utilizator: {{username}}", - "loadSharesFailed": "Nu s-au putut încărca partajările active" + "readWrite": "Citire-scriere" + } }, "terminal": { "connect": "Conectare la gazdă", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Trimite un număr zilnic anonim de utilizatori, gazde și utilizare a funcțiilor pentru a ajuta la îmbunătățirea Termix. Nu sunt incluse niciodată date personale sau detalii de conexiune.", "analyticsEnabledLockedDesc": "Această setare este blocată de variabila de mediu ENABLE_TELEMETRY și nu poate fi modificată aici.", "updateAnalyticsFailed": "Setările de analiză nu au putut fi actualizate", - "sessionSharingGloballyEnabled": "Permiteți partajarea sesiunii", "auditForwardingTitle": "Transmiteți către SIEM", "auditForwardingDesc": "Trimiteți o copie a fiecărei intrări de audit către un colector extern ca NDJSON. Jurnalele locale rămân sursa de adevăr.", "auditForwardingEnvNotice": "O variabilă de mediu configurează și redirecționarea; adresa URL salvată aici are prioritate.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Jeton la purtător (stocat)", "auditForwardingSaved": "Setările de redirecționare a auditului au fost salvate", "auditForwardingSaveError": "Setările de redirecționare a auditului nu au putut fi salvate.", - "sessionSharingGloballyEnabledDesc": "Permite partajarea sesiunilor live de terminal, RDP, VNC și Telnet la nivel de instanță. Suprascrie fiecare comutare de partajare per gazdă atunci când este dezactivată.", - "updateSessionSharingFailed": "Setările de partajare a sesiunii nu au putut fi actualizate.", "sessionTimeout": "Expirare sesiune", "hours": "ore", "sessionTimeoutRange": "Min 1 oră · Max 720 ore", diff --git a/src/ui/locales/translated/ru_RU.json b/src/ui/locales/translated/ru_RU.json index fdc38e7f6..893335f4a 100644 --- a/src/ui/locales/translated/ru_RU.json +++ b/src/ui/locales/translated/ru_RU.json @@ -667,7 +667,6 @@ "done_cli_link": "Откройте документацию по командной строке." }, "nav": { - "collab": "Встречи", "home": "Главная", "terminal": "Терминал", "localTerminal": "Локальный терминал", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Поддерживайте работоспособность вашей оболочки и всего, что в ней работает, даже при разрыве соединения и перезапуске браузера, запустив tmux (tmux должен быть установлен на хосте).", "enableSessionLogging": "Логирование сессии", "enableSessionLoggingDesc": "Записывать вывод терминальной сессии для последующего просмотра", - "allowSessionSharing": "Разрешить совместное использование сеанса", - "allowSessionSharingDesc": "Разрешите делиться записями онлайн-сессий на этом хостинге по ссылке или с другими пользователями.", "localEchoLabel": "Локальное эхо", "localEchoDesc": "Предсказывает безопасные нажатия клавиш для печати локально даже при медленном соединении. Запросы паролей и управляющий ввод никогда не отображаются.", "localEchoDefault": "Использовать глобальные настройки", @@ -1571,137 +1568,11 @@ "join": "Присоединиться", "sharedSessionLabel": "{{hostName}} (совместно)" }, - "collab": { - "createRoom": "Новая комната", - "roomName": "Название комнаты", - "persistentRoom": "Постоянное помещение", - "persistentRoomHint": "Список свободных комнат сохраняется и после окончания встречи, и их можно использовать повторно.", - "noRooms": "Комнат пока нет. Создайте комнату, чтобы начать встречу.", - "openRoom": "Открыть", - "endRoom": "Завершение встречи", - "leaveRoom": "Оставлять", - "deleteMember": "Удалять", - "removeMember": "Удалить {{name}}", - "removeMemberTitle": "Удалить этого участника?", - "removeMemberDescription": "{{name}} немедленно потеряет доступ к этой комнате. Любой активный SSH-просмотрщик будет отключен.", - "invite": "Приглашать", - "inviteTitle": "Пригласить участников", - "members": "Члены", - "membersPanel": "Участники комнаты и запросы на управление", - "membersWithCount": "Участники ({{count}})", - "online": "онлайн", - "offline": "офлайн", - "you": "(ты)", - "hostBadge": "Хозяин", - "presenterBadge": "Презентация", - "present": "Подарок", - "presentTitle": "Выходите на сцену", - "presentHost": "Хозяин", - "searchHosts": "Поиск хостов", - "noHostsFound": "Доступных хостов не найдено.", - "presentProtocol": "Протокол", - "stopPresenting": "Прекратите презентацию", - "takeOver": "Перенимать", - "takeOverConfirmTitle": "Заменить текущую презентацию?", - "takeOverDescription": "{{name}} сейчас выступает. Перехват инициативы отключит всех от этой сцены.", - "emptyStage": "Никто не выступает с презентацией. Выходите на сцену, чтобы поделиться своим опытом.", - "stageLoading": "Подключение к сцене...", - "stageEnded": "Презентация завершилась.", - "roomEnded": "Встреча завершена.", - "presenterLabel": "{{name}} представляет", - "guestLink": "Гостевая ссылка", - "guestLinkOn": "Гостевая ссылка включена. Любой, у кого есть ссылка, может посмотреть выступление.", - "guestLinkOff": "Гостевая ссылка отключена.", - "copyLink": "Скопировать ссылку", - "rotateLink": "Повернуть ссылку", - "disableLink": "Отключить ссылку", - "rotateLinkConfirmTitle": "Повернуть гостевую ссылку?", - "rotateLinkDescription": "Текущее соединение немедленно перестанет работать. Подключенные через SSH гостевые системы будут отключены.", - "disableLinkConfirmTitle": "Отключить гостевую ссылку?", - "disableLinkDescription": "Анонимные пользователи SSH-сервера будут немедленно отключены, и текущая связь перестанет работать.", - "linkCopied": "Ссылка скопирована", - "roles": "Роли", - "users": "Пользователи", - "searchUsers": "Поиск пользователей", - "endConfirmTitle": "Закончить это совещание?", - "endDescription": "{{name}} завершится для всех и не может быть возобновлено.", - "endPersistentDescription": "Активная презентация в {{name}} будет остановлена. Комната остается доступной для повторного использования.", - "deleteRoom": "Удалить комнату", - "deleteConfirmTitle": "Удалить эту комнату?", - "deleteDescription": "{{name}} будет безвозвратно удалено для всех и не может быть отменено.", - "invitedTo": "Вас пригласили на \"{{name}}\"", - "guest": { - "title": "Встреча", - "waiting": "Ожидание выступления докладчика...", - "linkInvalid": "Эта гостевая ссылка недействительна, или встреча завершилась." - }, - "reopenFromPanel": "Срок действия этой вкладки «Встречи» истек. Откройте комнату заново на панели «Встречи».", - "requestControl": "Управление запросами", - "cancelControlRequest": "Отменить запрос", - "controlQueue": "Запросы на управление", - "noControlRequests": "Нет ожидающих обработки запросов.", - "grantControlTo": "Передайте управление {{name}}", - "dismissControlRequest": "Отклонить запрос на управление от {{name}}", - "releaseControl": "Управление освобождением", - "grantControl": "Передайте контроль", - "revokeControl": "Верните себе контроль.", - "controlRequestedBy": "{{name}} запросил управление", - "grant": "Грант", - "controlBadge": "Под контролем", - "youArePresenting": "Вы ведёте презентацию в другой вкладке. Прекратите презентацию, чтобы передать слово другому пользователю.", - "created": "Комната создана", - "invited": "Приглашены члены", - "roomTab": "Встреча" - }, "sessionSharing": { - "guestView": { - "loading": "Подключение к общей сессии...", - "linkInvalid": "Данная ссылка для общего доступа недействительна, истек срок ее действия или она была аннулирована.", - "rateLimited": "Слишком много попыток, пожалуйста, попробуйте еще раз чуть позже.", - "sessionEnded": "Данная сессия завершена.", - "readOnlyBadge": "Только для просмотра", - "ownerLabel": "Владелец хостинга" - }, - "modalTitle": "Совместное использование сессии", - "shareButton": "Делиться", - "notReadyToShare": "Сессия пока не готова к публикации.", - "modeTab": { - "link": "Связь", - "user": "Пользователь" - }, - "linkModeDescription": "Присоединиться может любой, у кого есть эта ссылка, регистрация не требуется.", - "userModeDescription": "Предоставьте доступ конкретному пользователю, у которого уже есть доступ к этому хосту. Если у него еще нет доступа, сначала предоставьте ему доступ к хосту или используйте ссылку. После предоставления доступа сессия отобразится на вкладке «Подключения» этого пользователя.", "permissionLevel": { - "label": "Уровень доступа", "readOnly": "Только для чтения", - "readOnlyDescription": "Можно наблюдать за сессией в прямом эфире, но нельзя печатать или взаимодействовать с ней.", - "readWrite": "Чтение-запись", - "readWriteDescription": "Может печатать и взаимодействовать с сессией так же, как и её владелец." - }, - "expiryLabel": "Срок действия ссылки истекает", - "createLinkButton": "Создать ссылку", - "createShareButton": "Поделиться с пользователем", - "searchUsersPlaceholder": "Поиск пользователей...", - "noUsersFound": "Пользователи не найдены", - "linkCreated": "Ссылка для обмена создана", - "linkCopied": "Ссылка скопирована в буфер обмена", - "copyLink": "Скопировать ссылку", - "shareCreated": "Сессия была проведена совместно. Она отобразится на вкладке «Подключения».", - "shareFailed": "Не удалось создать общий ресурс.", - "userLacksHostAccess": "У этого пользователя пока нет доступа к данному хосту. Сначала предоставьте ему доступ к хосту или используйте ссылку.", - "activeShares": "Активные акции", - "noActiveShares": "В текущей торговой сессии нет активных акций.", - "revoke": "Отменить", - "revokeConfirmTitle": "Отозвать эту публикацию?", - "revokeConfirmDescription": "Любой, кто воспользуется этой ссылкой, немедленно потеряет к ней доступ.", - "revoked": "Акция аннулирована", - "revokeFailed": "Не удалось отозвать акцию.", - "joinCount": "{{count}} присоединиться", - "joinCount_other": "{{count}} присоединяется", - "expiresAt": "Срок действия истекает {{date}}", - "linkShareBadge": "Связь", - "userShareBadge": "Пользователь: {{username}}", - "loadSharesFailed": "Не удалось загрузить активные акции." + "readWrite": "Чтение-запись" + } }, "terminal": { "connect": "Подключиться к хосту", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Ежедневно отправляет анонимный подсчет пользователей, хостов и использования функций, чтобы помочь улучшить Termix. Никакие личные данные или сведения о подключении никогда не включаются.", "analyticsEnabledLockedDesc": "Этот параметр заблокирован переменной среды ENABLE_TELEMETRY и не может быть изменен здесь.", "updateAnalyticsFailed": "Не удалось обновить настройки аналитики.", - "sessionSharingGloballyEnabled": "Разрешить совместное использование сеанса", "auditForwardingTitle": "Переслать в SIEM", "auditForwardingDesc": "Отправляйте копию каждой записи аудита внешнему сборщику данных в формате NDJSON. Локальные журналы остаются источником достоверной информации.", "auditForwardingEnvNotice": "Переменная среды также настраивает переадресацию; URL-адрес, сохраненный здесь, имеет приоритет.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Токен носителя (сохраненный)", "auditForwardingSaved": "Сохранены настройки пересылки аудита.", "auditForwardingSaveError": "Не удалось сохранить настройки пересылки аудита.", - "sessionSharingGloballyEnabledDesc": "Разрешить общий доступ к сеансам Live Terminal, RDP, VNC и Telnet для всего экземпляра. При отключении этой функции отменяет все настройки общего доступа для отдельных хостов.", - "updateSessionSharingFailed": "Не удалось обновить настройки совместного доступа к сессии.", "sessionTimeout": "Тайм-аут сессии", "hours": "часов", "sessionTimeoutRange": "Мин. 1 ч · Макс. 720 ч", diff --git a/src/ui/locales/translated/sr_SP.json b/src/ui/locales/translated/sr_SP.json index 21049aca7..f765a6094 100644 --- a/src/ui/locales/translated/sr_SP.json +++ b/src/ui/locales/translated/sr_SP.json @@ -667,7 +667,6 @@ "done_cli_link": "Отворите CLI документацију" }, "nav": { - "collab": "Састанци", "home": "Дом", "terminal": "Терминал", "localTerminal": "Локални терминал", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Одржавајте своју шкољку и све што се у њој покреће активним након прекида везе и поновног покретања прегледача покретањем унутар tmux-а (tmux мора бити инсталиран на хосту)", "enableSessionLogging": "Записивање сесије", "enableSessionLoggingDesc": "Снимите излаз терминалне сесије за каснији преглед", - "allowSessionSharing": "Дозволи дељење сесије", - "allowSessionSharingDesc": "Дозволите дељење сесија уживо на овом хосту путем линка или са другим корисницима", "localEchoLabel": "Локални одјек", "localEchoDesc": "Предвиди безбедне притиске тастера за штампање локално на спорим везама. Упити за лозинку и контролни уноси се никада не понављају.", "localEchoDefault": "Користите глобална подешавања", @@ -1571,137 +1568,11 @@ "join": "Придружи се", "sharedSessionLabel": "{{hostName}} (дељено)" }, - "collab": { - "createRoom": "Нова соба", - "roomName": "Назив собе", - "persistentRoom": "Трајна соба", - "persistentRoomHint": "Трајне собе остају наведене након завршетка састанка и могу се поново користити.", - "noRooms": "Још нема соба. Направите једну да бисте започели састанак.", - "openRoom": "Отворено", - "endRoom": "Заврши састанак", - "leaveRoom": "Одиђи", - "deleteMember": "Уклони", - "removeMember": "Уклони {{name}}", - "removeMemberTitle": "Уклонити овог члана?", - "removeMemberDescription": "{{name}} ће одмах изгубити приступ овој соби. Сваки активни SSH гледалац ће бити искључен.", - "invite": "Позови", - "inviteTitle": "Позовите чланове", - "members": "Чланови", - "membersPanel": "Чланови собе и захтеви за контролу", - "membersWithCount": "Чланови ({{count}})", - "online": "онлајн", - "offline": "офлајн", - "you": "(ти)", - "hostBadge": "Домаћин", - "presenterBadge": "Презентација", - "present": "Присутан", - "presentTitle": "Изађи на сцену", - "presentHost": "Домаћин", - "searchHosts": "Претрага хостова", - "noHostsFound": "Није пронађен ниједан доступан хост.", - "presentProtocol": "Протокол", - "stopPresenting": "Заустави презентацију", - "takeOver": "Преузми", - "takeOverConfirmTitle": "Заменити тренутну презентацију?", - "takeOverDescription": "{{name}} тренутно презентује. Преузимање ће искључити све са те сцене.", - "emptyStage": "Нико не презентује. Изађите на бину да бисте поделили сесију.", - "stageLoading": "Повезивање са сценом...", - "stageEnded": "Презентација је завршена", - "roomEnded": "Овај састанак је завршен", - "presenterLabel": "{{name}} презентује", - "guestLink": "Гостујући линк", - "guestLinkOn": "Линк за госте је укључен. Свако ко има линк може да гледа сцену.", - "guestLinkOff": "Линк за госте је искључен.", - "copyLink": "Копирај линк", - "rotateLink": "Ротирај линк", - "disableLink": "Онемогући везу", - "rotateLinkConfirmTitle": "Ротирати линк за госте?", - "rotateLinkDescription": "Тренутна веза ће одмах престати да ради. Повезани SSH гости ће бити искључени.", - "disableLinkConfirmTitle": "Онемогућити линк за госте?", - "disableLinkDescription": "Анонимни SSH гледаоци ће одмах бити прекинути везу и тренутна веза ће престати да ради.", - "linkCopied": "Линк је копиран", - "roles": "Улоге", - "users": "Корисници", - "searchUsers": "Претрага корисника", - "endConfirmTitle": "Да ли желите да завршите овај састанак?", - "endDescription": "{{name}} ће бити завршен за све и не може се поново отворити.", - "endPersistentDescription": "Активна презентација у {{name}} ће се зауставити. Просторија остаје доступна за поновну употребу.", - "deleteRoom": "Обриши собу", - "deleteConfirmTitle": "Да ли желите да избришете ову собу?", - "deleteDescription": "{{name}} ће бити трајно обрисан за све и не може се поништити.", - "invitedTo": "Позвани сте у „{{name}}“", - "guest": { - "title": "Састанак", - "waiting": "Чекање водитеља...", - "linkInvalid": "Овај линк за госте је неважећи или је састанак завршен" - }, - "reopenFromPanel": "Ова картица састанка је истекла. Поново отворите просторију из панела Састанци.", - "requestControl": "Захтевај контролу", - "cancelControlRequest": "Откажи захтев", - "controlQueue": "Захтеви за контролу", - "noControlRequests": "Нема захтева на чекању.", - "grantControlTo": "Дајте контролу кориснику {{name}}", - "dismissControlRequest": "Одбаци захтев за контролу корисника {{name}}", - "releaseControl": "Контрола ослобађања", - "grantControl": "Дајте контролу", - "revokeControl": "Преузмите контролу назад", - "controlRequestedBy": "{{name}} је затражио контролу", - "grant": "Грант", - "controlBadge": "Под контролом", - "youArePresenting": "Презентујете у другој картици. Прекините презентацију да бисте предали сцену.", - "created": "Соба је направљена", - "invited": "Чланови позвани", - "roomTab": "Састанак" - }, "sessionSharing": { - "guestView": { - "loading": "Повезивање са дељеном сесијом...", - "linkInvalid": "Овај линк за дељење је неважећи, истекао је или је опозван", - "rateLimited": "Превише покушаја, покушајте поново ускоро", - "sessionEnded": "Ова сесија је завршена", - "readOnlyBadge": "Само преглед", - "ownerLabel": "Власник домаћина" - }, - "modalTitle": "Дели сесију", - "shareButton": "Дели", - "notReadyToShare": "Сесија још није спремна за дељење", - "modeTab": { - "link": "Линк", - "user": "Корисник" - }, - "linkModeDescription": "Свако ко има овај линк може да се придружи, није потребан налог.", - "userModeDescription": "Делите са одређеним корисником који већ има приступ овом хосту. Ако још немају приступ, прво поделите хост са њима или користите линк. Након дељења, сесија се појављује на њиховој картици „Везе“.", "permissionLevel": { - "label": "Ниво дозволе", "readOnly": "Само за читање", - "readOnlyDescription": "Може да гледа сесију уживо, али не може да куца или да комуницира.", - "readWrite": "Читање и писање", - "readWriteDescription": "Може да куца и интерагује са сесијом баш као и власник." - }, - "expiryLabel": "Истек линка", - "createLinkButton": "Направи линк", - "createShareButton": "Подели са корисником", - "searchUsersPlaceholder": "Претражи кориснике...", - "noUsersFound": "Није пронађен ниједан корисник", - "linkCreated": "Линк за дељење је направљен", - "linkCopied": "Линк је копиран у међуспремник", - "copyLink": "Копирај линк", - "shareCreated": "Сесија је дељена. Појавиће се на њиховој картици „Везе“.", - "shareFailed": "Није успело креирање дељења", - "userLacksHostAccess": "Тај корисник још увек нема приступ овом хосту. Прво поделите хост са њим или користите линк.", - "activeShares": "Активне акције", - "noActiveShares": "Нема активних дељења за ову сесију", - "revoke": "Поништи", - "revokeConfirmTitle": "Да ли желите да опозовете ово дељење?", - "revokeConfirmDescription": "Свако ко користи ово дељење ће одмах изгубити приступ.", - "revoked": "Дељење опозвано", - "revokeFailed": "Опозивање дељења није успело", - "joinCount": "{{count}} придружи се", - "joinCount_other": "{{count}} придруживања", - "expiresAt": "Истиче {{date}}", - "linkShareBadge": "Линк", - "userShareBadge": "Корисник: {{username}}", - "loadSharesFailed": "Учитавање активних дељења није успело" + "readWrite": "Читање и писање" + } }, "terminal": { "connect": "Повежи се са хостом", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Шаље анонимни дневни број корисника, хостова и коришћења функција како би се побољшао Termix. Никада се не укључују лични подаци или детаљи везе.", "analyticsEnabledLockedDesc": "Ово подешавање је закључано променљивом окружења ENABLE_TELEMETRY и не може се овде променити.", "updateAnalyticsFailed": "Ажурирање подешавања аналитике није успело", - "sessionSharingGloballyEnabled": "Дозволи дељење сесије", "auditForwardingTitle": "Проследи ка SIEM-у", "auditForwardingDesc": "Пошаљите копију сваког уноса ревизије екстерном колектору као NDJSON. Локални логови остају извор истине.", "auditForwardingEnvNotice": "Променљива окружења такође конфигурише прослеђивање; URL сачуван овде има предност.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Токен носиоца (сачуван)", "auditForwardingSaved": "Подешавања прослеђивања ревизије сачувана", "auditForwardingSaveError": "Чување подешавања прослеђивања ревизије није успело", - "sessionSharingGloballyEnabledDesc": "Дозвољава дељење сесија уживо терминала, RDP, VNC и Telnet сесија на нивоу целе инстанце. Замењује сваки прекидач за дељење по хосту када је онемогућен.", - "updateSessionSharingFailed": "Ажурирање подешавања дељења сесије није успело", "sessionTimeout": "Временско ограничење сесије", "hours": "сати", "sessionTimeoutRange": "Мин. 1 сат · Макс. 720 сати", diff --git a/src/ui/locales/translated/sv_SE.json b/src/ui/locales/translated/sv_SE.json index 5b1bf3215..70d116962 100644 --- a/src/ui/locales/translated/sv_SE.json +++ b/src/ui/locales/translated/sv_SE.json @@ -667,7 +667,6 @@ "done_cli_link": "Öppna CLI-dokumentationen" }, "nav": { - "collab": "Möten", "home": "Hem", "terminal": "Terminal", "localTerminal": "Lokal terminal", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Håll ditt skal och allt som körs i det vid liv även efter frånkopplingar och omstart av webbläsare genom att köra det inuti tmux (tmux måste vara installerat på värden)", "enableSessionLogging": "Sessionsloggning", "enableSessionLoggingDesc": "Spela in utdata från terminalsessionen för senare granskning", - "allowSessionSharing": "Tillåt sessionsdelning", - "allowSessionSharingDesc": "Tillåt att livesessioner på den här värden delas via länk eller med andra användare", "localEchoLabel": "Lokalt eko", "localEchoDesc": "Förutsäg säkra utskrivbara tangenttryckningar lokalt på långsamma anslutningar. Lösenordsförfrågningar och kontrollinmatning upprepas aldrig.", "localEchoDefault": "Använd global inställning", @@ -1571,137 +1568,11 @@ "join": "Ansluta sig till", "sharedSessionLabel": "{{hostName}} (delad)" }, - "collab": { - "createRoom": "Nytt rum", - "roomName": "Rumsnamn", - "persistentRoom": "Permanent rum", - "persistentRoomHint": "Beständiga rum finns kvar i listan efter att mötet är slut och kan återanvändas.", - "noRooms": "Inga rum ännu. Skapa ett för att starta ett möte.", - "openRoom": "Öppna", - "endRoom": "Avsluta mötet", - "leaveRoom": "Lämna", - "deleteMember": "Ta bort", - "removeMember": "Ta bort {{name}}", - "removeMemberTitle": "Ta bort den här medlemmen?", - "removeMemberDescription": "{{name}} kommer att förlora åtkomst till det här rummet omedelbart. Alla aktiva SSH-läsare kommer att kopplas bort.", - "invite": "Bjuda", - "inviteTitle": "Bjud in medlemmar", - "members": "Medlemmar", - "membersPanel": "Rumsmedlemmar och kontrollförfrågningar", - "membersWithCount": "Medlemmar ({{count}})", - "online": "online", - "offline": "off-line", - "you": "(du)", - "hostBadge": "Värd", - "presenterBadge": "Presenterar", - "present": "Presentera", - "presentTitle": "Inta scenen", - "presentHost": "Värd", - "searchHosts": "Sök värdar", - "noHostsFound": "Inga tillgängliga värdar hittades.", - "presentProtocol": "Protokoll", - "stopPresenting": "Sluta presentera", - "takeOver": "Ta över", - "takeOverConfirmTitle": "Ersätta den aktuella presentationen?", - "takeOverDescription": "{{name}} presenterar nu. Att ta över kommer att koppla bort alla från den scenen.", - "emptyStage": "Ingen presenterar. Gå ut på scenen för att dela en session.", - "stageLoading": "Ansluter till scenen...", - "stageEnded": "Presentationen avslutades", - "roomEnded": "Det här mötet har avslutats", - "presenterLabel": "{{name}} presenterar", - "guestLink": "Gästlänk", - "guestLinkOn": "Gästlänken är aktiverad. Alla med länken kan titta på scenen.", - "guestLinkOff": "Gästlänken är avstängd.", - "copyLink": "Kopiera länk", - "rotateLink": "Rotera länken", - "disableLink": "Inaktivera länk", - "rotateLinkConfirmTitle": "Rotera gästlänken?", - "rotateLinkDescription": "Den aktuella länken kommer att sluta fungera omedelbart. Anslutna SSH-gäster kommer att kopplas bort.", - "disableLinkConfirmTitle": "Inaktivera gästlänken?", - "disableLinkDescription": "Anonyma SSH-användare kommer att kopplas bort omedelbart och den aktuella länken kommer att sluta fungera.", - "linkCopied": "Länk kopierad", - "roles": "Roller", - "users": "Användare", - "searchUsers": "Sök användare", - "endConfirmTitle": "Avsluta det här mötet?", - "endDescription": "{{name}} kommer att upphöra för alla och kan inte öppnas igen.", - "endPersistentDescription": "Den aktiva presentationen i {{name}} kommer att stoppas. Rummet är fortfarande tillgängligt för återanvändning.", - "deleteRoom": "Ta bort rum", - "deleteConfirmTitle": "Radera det här rummet?", - "deleteDescription": "{{name}} kommer att raderas permanent för alla och kan inte ångras.", - "invitedTo": "Du blev inbjuden till \"{{name}}\"", - "guest": { - "title": "Möte", - "waiting": "Väntar på programledaren...", - "linkInvalid": "Denna gästlänk är ogiltig eller så har mötet avslutats" - }, - "reopenFromPanel": "Den här mötesfliken har upphört att gälla. Öppna rummet igen från panelen Möten.", - "requestControl": "Begäran om kontroll", - "cancelControlRequest": "Avbryt begäran", - "controlQueue": "Kontrollförfrågningar", - "noControlRequests": "Inga väntande förfrågningar.", - "grantControlTo": "Ge kontroll till {{name}}", - "dismissControlRequest": "Avvisa kontrollbegäran från {{name}}", - "releaseControl": "Släppkontroll", - "grantControl": "Ge kontroll", - "revokeControl": "Ta tillbaka kontrollen", - "controlRequestedBy": "{{name}} bad om kontroll", - "grant": "Bevilja", - "controlBadge": "I kontroll", - "youArePresenting": "Du presenterar i en annan flik. Avbryt presentationen för att lämna över scenen.", - "created": "Rum skapat", - "invited": "Medlemmar inbjudna", - "roomTab": "Möte" - }, "sessionSharing": { - "guestView": { - "loading": "Ansluter till delad session...", - "linkInvalid": "Den här delningslänken är ogiltig, har löpt ut eller har återkallats", - "rateLimited": "För många försök, försök igen snart", - "sessionEnded": "Den här sessionen är avslutad", - "readOnlyBadge": "Endast visning", - "ownerLabel": "Värdägare" - }, - "modalTitle": "Dela session", - "shareButton": "Dela", - "notReadyToShare": "Sessionen är inte redo att delas ännu", - "modeTab": { - "link": "Länk", - "user": "Användare" - }, - "linkModeDescription": "Vem som helst med den här länken kan gå med, inget konto krävs.", - "userModeDescription": "Dela med en specifik användare som redan har åtkomst till den här värden. Om de inte har åtkomst ännu, dela värden med dem först eller använd en länk istället. När sessionen har delats visas den på deras flik Anslutningar.", "permissionLevel": { - "label": "Behörighetsnivå", "readOnly": "Skrivskyddad", - "readOnlyDescription": "Kan titta på sessionen live men kan inte skriva eller interagera.", - "readWrite": "Läs-skriv", - "readWriteDescription": "Kan skriva och interagera med sessionen precis som ägaren." - }, - "expiryLabel": "Länkens utgångsdatum", - "createLinkButton": "Skapa länk", - "createShareButton": "Dela med användaren", - "searchUsersPlaceholder": "Sök användare...", - "noUsersFound": "Inga användare hittades", - "linkCreated": "Delningslänk skapad", - "linkCopied": "Länk kopierad till urklipp", - "copyLink": "Kopiera länk", - "shareCreated": "Sessionen delade. Den kommer att visas på fliken Anslutningar.", - "shareFailed": "Misslyckades med att skapa delningen", - "userLacksHostAccess": "Den användaren har inte åtkomst till den här värden ännu. Dela värden med dem först, eller använd en länk istället.", - "activeShares": "Aktiva aktier", - "noActiveShares": "Inga aktiva delningar för den här sessionen", - "revoke": "Återkalla", - "revokeConfirmTitle": "Återkalla den här delningen?", - "revokeConfirmDescription": "Alla som använder den här delningen kommer att förlora åtkomsten omedelbart.", - "revoked": "Delning återkallad", - "revokeFailed": "Misslyckades med att återkalla delningen", - "joinCount": "{{count}} gå med", - "joinCount_other": "{{count}} ansluter sig", - "expiresAt": "Utgångsdatum {{date}}", - "linkShareBadge": "Länk", - "userShareBadge": "Användare: {{username}}", - "loadSharesFailed": "Misslyckades med att läsa in aktiva delningar" + "readWrite": "Läs-skriv" + } }, "terminal": { "connect": "Anslut till värd", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Skickar en anonym daglig räkning av användare, värdar och funktionsanvändning för att förbättra Termix. Inga personuppgifter eller anslutningsdetaljer inkluderas någonsin.", "analyticsEnabledLockedDesc": "Den här inställningen är låst av miljövariabeln ENABLE_TELEMETRY och kan inte ändras här.", "updateAnalyticsFailed": "Det gick inte att uppdatera analysinställningen", - "sessionSharingGloballyEnabled": "Tillåt sessionsdelning", "auditForwardingTitle": "Vidarebefordra till SIEM", "auditForwardingDesc": "Skicka en kopia av varje granskningspost till en extern insamlare som NDJSON. Lokala loggar förblir källan till sanningen.", "auditForwardingEnvNotice": "En miljövariabel konfigurerar också vidarebefordran; URL:en som sparas här har företräde.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Bärartoken (lagrad)", "auditForwardingSaved": "Inställningar för vidarebefordran av granskning har sparats", "auditForwardingSaveError": "Det gick inte att spara inställningarna för vidarebefordran av granskning", - "sessionSharingGloballyEnabledDesc": "Tillåt att liveterminal-, RDP-, VNC- och Telnet-sessioner delas instansövergripande. Åsidosätter alla delningsknappar per värd när de är inaktiverade.", - "updateSessionSharingFailed": "Det gick inte att uppdatera inställningen för sessionsdelning", "sessionTimeout": "Sessionstimeout", "hours": "timmar", "sessionTimeoutRange": "Min 1 timme · Max 720 timmar", diff --git a/src/ui/locales/translated/th_TH.json b/src/ui/locales/translated/th_TH.json index 09bb4b50d..d0a6b7dc0 100644 --- a/src/ui/locales/translated/th_TH.json +++ b/src/ui/locales/translated/th_TH.json @@ -667,7 +667,6 @@ "done_cli_link": "เปิดเอกสาร CLI" }, "nav": { - "collab": "การประชุม", "home": "บ้าน", "terminal": "เทอร์มินัล", "localTerminal": "สถานีปลายทางท้องถิ่น", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "รักษาการทำงานของเชลล์และโปรแกรมใดๆ ที่ทำงานอยู่ภายในเชลล์ให้คงอยู่ต่อไปแม้การตัดการเชื่อมต่อหรือการรีสตาร์ทเบราว์เซอร์ โดยการเรียกใช้คำสั่งภายใน tmux (ต้องติดตั้ง tmux บนเครื่องโฮสต์)", "enableSessionLogging": "การบันทึกเซสชัน", "enableSessionLoggingDesc": "บันทึกผลลัพธ์จากเทอร์มินัลเพื่อตรวจสอบในภายหลัง", - "allowSessionSharing": "อนุญาตการแชร์เซสชัน", - "allowSessionSharingDesc": "อนุญาตให้แชร์เซสชันสดบนโฮสต์นี้ผ่านลิงก์หรือกับผู้ใช้รายอื่น", "localEchoLabel": "เสียงสะท้อนในท้องถิ่น", "localEchoDesc": "คาดการณ์การกดแป้นพิมพ์ที่ปลอดภัยสำหรับการพิมพ์ในเครื่องบนการเชื่อมต่อที่ช้า ข้อความแจ้งให้ป้อนรหัสผ่านและการป้อนข้อมูลควบคุมจะไม่ถูกแสดงซ้ำ", "localEchoDefault": "ใช้การตั้งค่าทั่วโลก", @@ -1571,137 +1568,11 @@ "join": "เข้าร่วม", "sharedSessionLabel": "{{hostName}} (แชร์)" }, - "collab": { - "createRoom": "ห้องใหม่", - "roomName": "ชื่อห้อง", - "persistentRoom": "ห้องที่คงอยู่", - "persistentRoomHint": "ห้องประชุมที่ตั้งค่าไว้ถาวรจะยังคงอยู่ในรายการหลังจากสิ้นสุดการประชุมและสามารถนำกลับมาใช้ใหม่ได้", - "noRooms": "ยังไม่มีห้องประชุม สร้างห้องประชุมเพื่อเริ่มการประชุม", - "openRoom": "เปิด", - "endRoom": "สิ้นสุดการประชุม", - "leaveRoom": "ออกจาก", - "deleteMember": "ลบ", - "removeMember": "ลบ {{name}}", - "removeMemberTitle": "ลบสมาชิกนี้ออก?", - "removeMemberDescription": "{{name}} จะเสียสิทธิ์ในการเข้าถึงห้องนี้ทันที โปรแกรมดู SSH ที่ใช้งานอยู่จะถูกตัดการเชื่อมต่อ", - "invite": "เชิญ", - "inviteTitle": "เชิญสมาชิก", - "members": "สมาชิก", - "membersPanel": "สมาชิกห้องและคำขอควบคุม", - "membersWithCount": "สมาชิก ({{count}})", - "online": "ออนไลน์", - "offline": "ออฟไลน์", - "you": "(คุณ)", - "hostBadge": "เจ้าภาพ", - "presenterBadge": "นำเสนอ", - "present": "ปัจจุบัน", - "presentTitle": "ขึ้นเวที", - "presentHost": "เจ้าภาพ", - "searchHosts": "ค้นหาโฮสต์", - "noHostsFound": "ไม่พบโฮสต์ที่ใช้งานได้", - "presentProtocol": "โปรโตคอล", - "stopPresenting": "หยุดการนำเสนอ", - "takeOver": "เข้ายึดครอง", - "takeOverConfirmTitle": "ต้องการเปลี่ยนรูปแบบการนำเสนอปัจจุบันหรือไม่?", - "takeOverDescription": "{{name}} กำลังนำเสนออยู่ การเข้าควบคุมจะตัดการเชื่อมต่อทุกคนจากเวทีนั้น", - "emptyStage": "ไม่มีใครกำลังนำเสนออยู่ เชิญขึ้นเวทีเพื่อร่วมแบ่งปันช่วงการนำเสนอได้เลย", - "stageLoading": "เชื่อมต่อกับเวที...", - "stageEnded": "การนำเสนอสิ้นสุดลงแล้ว", - "roomEnded": "การประชุมนี้ได้สิ้นสุดลงแล้ว", - "presenterLabel": "{{name}} กำลังนำเสนอ", - "guestLink": "ลิงก์สำหรับแขก", - "guestLinkOn": "ลิงก์สำหรับแขกรับเชิญเปิดใช้งานแล้ว ใครก็ตามที่มีลิงก์สามารถรับชมการแสดงสดได้", - "guestLinkOff": "ลิงก์สำหรับผู้เยี่ยมชมถูกปิดใช้งาน", - "copyLink": "คัดลิงก์", - "rotateLink": "หมุนลิงก์", - "disableLink": "ปิดใช้งานลิงก์", - "rotateLinkConfirmTitle": "หมุนลิงก์สำหรับแขกรับเชิญหรือไม่?", - "rotateLinkDescription": "ลิงก์ปัจจุบันจะหยุดทำงานทันที ผู้ใช้งาน SSH ที่เชื่อมต่ออยู่จะถูกตัดการเชื่อมต่อ", - "disableLinkConfirmTitle": "ปิดใช้งานลิงก์สำหรับแขกใช่ไหม?", - "disableLinkDescription": "ผู้ใช้งาน SSH ที่ไม่ระบุตัวตนจะถูกตัดการเชื่อมต่อทันที และลิงก์ปัจจุบันจะหยุดทำงาน", - "linkCopied": "คัดลอกลิงก์แล้ว", - "roles": "บทบาท", - "users": "ผู้ใช้", - "searchUsers": "ค้นหาผู้ใช้", - "endConfirmTitle": "จบการประชุมนี้?", - "endDescription": "{{name}} จะสิ้นสุดลงสำหรับทุกคนและไม่สามารถเปิดใช้งานใหม่ได้อีก", - "endPersistentDescription": "การนำเสนอใน {{name}} จะหยุดลง ห้องยังคงพร้อมใช้งานได้ตามปกติ", - "deleteRoom": "ลบห้อง", - "deleteConfirmTitle": "ลบห้องนี้?", - "deleteDescription": "{{name}} จะถูกลบอย่างถาวรสำหรับทุกคนและไม่สามารถกู้คืนได้", - "invitedTo": "คุณได้รับเชิญไปที่ \"{{name}}\"", - "guest": { - "title": "การประชุม", - "waiting": "กำลังรอผู้ประกาศ...", - "linkInvalid": "ลิงก์สำหรับผู้เข้าร่วมนี้ไม่ถูกต้อง หรือการประชุมได้สิ้นสุดลงแล้ว" - }, - "reopenFromPanel": "แท็บการประชุมนี้หมดอายุแล้ว โปรดเปิดห้องประชุมอีกครั้งจากแผงการประชุม", - "requestControl": "การควบคุมคำขอ", - "cancelControlRequest": "คำขอการยกเลิก", - "controlQueue": "คำขอควบคุม", - "noControlRequests": "ไม่มีคำขอที่รอการดำเนินการ", - "grantControlTo": "มอบการควบคุมให้กับ {{name}}", - "dismissControlRequest": "ยกเลิกคำขอควบคุมของ {{name}}", - "releaseControl": "ปล่อยการควบคุม", - "grantControl": "มอบการควบคุม", - "revokeControl": "ทวงคืนการควบคุม", - "controlRequestedBy": "{{name}} ขอควบคุม", - "grant": "ยินยอม", - "controlBadge": "ควบคุมได้", - "youArePresenting": "คุณกำลังนำเสนออยู่ในแท็บอื่น โปรดหยุดการนำเสนอเพื่อส่งต่อให้ผู้อื่น", - "created": "สร้างห้อง", - "invited": "สมาชิกที่ได้รับเชิญ", - "roomTab": "การประชุม" - }, "sessionSharing": { - "guestView": { - "loading": "กำลังเชื่อมต่อกับเซสชันที่ใช้ร่วมกัน...", - "linkInvalid": "ลิงก์แชร์นี้ไม่ถูกต้อง หมดอายุ หรือถูกยกเลิกแล้ว", - "rateLimited": "พยายามหลายครั้งเกินไป โปรดลองใหม่อีกครั้งในภายหลัง", - "sessionEnded": "การประชุมครั้งนี้สิ้นสุดลงแล้ว", - "readOnlyBadge": "ดูได้อย่างเดียว", - "ownerLabel": "เจ้าของที่พัก" - }, - "modalTitle": "เซสชั่นการแบ่งปัน", - "shareButton": "แบ่งปัน", - "notReadyToShare": "เซสชั่นยังไม่พร้อมให้แชร์", - "modeTab": { - "link": "ลิงก์", - "user": "ผู้ใช้" - }, - "linkModeDescription": "ใครก็ตามที่มีลิงก์นี้สามารถเข้าร่วมได้ ไม่จำเป็นต้องมีบัญชีผู้ใช้", - "userModeDescription": "แชร์กับผู้ใช้เฉพาะรายที่มีสิทธิ์เข้าถึงโฮสต์นี้อยู่แล้ว หากพวกเขายังไม่มีสิทธิ์เข้าถึง ให้แชร์โฮสต์กับพวกเขาก่อน หรือใช้ลิงก์แทน เมื่อแชร์แล้ว เซสชันจะปรากฏในแท็บการเชื่อมต่อของพวกเขา", "permissionLevel": { - "label": "ระดับการอนุญาต", "readOnly": "อ่านอย่างเดียว", - "readOnlyDescription": "สามารถรับชมการประชุมสดได้ แต่ไม่สามารถพิมพ์หรือโต้ตอบได้", - "readWrite": "อ่าน-เขียน", - "readWriteDescription": "สามารถพิมพ์และโต้ตอบกับเซสชันได้เหมือนกับเจ้าของเซสชัน" - }, - "expiryLabel": "ลิงก์หมดอายุ", - "createLinkButton": "สร้างลิงก์", - "createShareButton": "แชร์กับผู้ใช้", - "searchUsersPlaceholder": "ค้นหาผู้ใช้...", - "noUsersFound": "ไม่พบผู้ใช้งาน", - "linkCreated": "สร้างลิงก์สำหรับแชร์แล้ว", - "linkCopied": "คัดลอกลิงก์ไปยังคลิปบอร์ดแล้ว", - "copyLink": "คัดลิงก์", - "shareCreated": "แชร์เซสชันแล้ว เซสชันนั้นจะปรากฏในแท็บการเชื่อมต่อของพวกเขา", - "shareFailed": "ไม่สามารถสร้างการแชร์ได้", - "userLacksHostAccess": "ผู้ใช้รายนั้นยังไม่มีสิทธิ์เข้าถึงโฮสต์นี้ โปรดแชร์โฮสต์ให้พวกเขาก่อน หรือใช้ลิงก์แทน", - "activeShares": "หุ้นที่ใช้งานอยู่", - "noActiveShares": "ไม่มีหุ้นที่เปิดซื้อขายในรอบนี้", - "revoke": "ถอน", - "revokeConfirmTitle": "ต้องการยกเลิกการแชร์นี้หรือไม่?", - "revokeConfirmDescription": "ใครก็ตามที่ใช้ลิงก์นี้จะเสียสิทธิ์การเข้าถึงทันที", - "revoked": "หุ้นถูกเพิกถอน", - "revokeFailed": "ไม่สามารถเพิกถอนหุ้นได้", - "joinCount": "{{count}} เข้าร่วม", - "joinCount_other": "{{count}} เข้าร่วม", - "expiresAt": "หมดอายุ {{date}}", - "linkShareBadge": "ลิงก์", - "userShareBadge": "ผู้ใช้: {{username}}", - "loadSharesFailed": "ไม่สามารถโหลดหุ้นที่ใช้งานอยู่ได้" + "readWrite": "อ่าน-เขียน" + } }, "terminal": { "connect": "เชื่อมต่อกับโฮสต์", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "ส่งรายงานจำนวนผู้ใช้ โฮสต์ และการใช้งานฟีเจอร์รายวันแบบไม่ระบุตัวตน เพื่อช่วยปรับปรุง Termix ไม่มีข้อมูลส่วนบุคคลหรือรายละเอียดการเชื่อมต่อใดๆ รวมอยู่ด้วย", "analyticsEnabledLockedDesc": "การตั้งค่านี้ถูกล็อกโดยตัวแปรสภาพแวดล้อม ENABLE_TELEMETRY และไม่สามารถเปลี่ยนแปลงได้ที่นี่", "updateAnalyticsFailed": "ไม่สามารถอัปเดตการตั้งค่าการวิเคราะห์ได้", - "sessionSharingGloballyEnabled": "อนุญาตการแชร์เซสชัน", "auditForwardingTitle": "ส่งต่อไปยัง SIEM", "auditForwardingDesc": "ส่งสำเนาของรายการตรวจสอบทุกรายการไปยังตัวเก็บรวบรวมภายนอกในรูปแบบ NDJSON บันทึกในเครื่องยังคงเป็นแหล่งข้อมูลที่ถูกต้องที่สุด", "auditForwardingEnvNotice": "ตัวแปรสภาพแวดล้อมยังใช้กำหนดค่าการส่งต่อด้วย โดย URL ที่บันทึกไว้ในที่นี้จะมีลำดับความสำคัญเหนือกว่า", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "โทเค็นผู้ถือ (ที่จัดเก็บไว้)", "auditForwardingSaved": "บันทึกการตั้งค่าการส่งต่อการตรวจสอบแล้ว", "auditForwardingSaveError": "ไม่สามารถบันทึกการตั้งค่าการส่งต่อการตรวจสอบได้", - "sessionSharingGloballyEnabledDesc": "อนุญาตให้แชร์เซสชันเทอร์มินัลสด, RDP, VNC และ Telnet ทั่วทั้งอินสแตนซ์ การตั้งค่านี้จะแทนที่การตั้งค่าการแชร์ต่อโฮสต์ทั้งหมดเมื่อปิดใช้งานอยู่", - "updateSessionSharingFailed": "ไม่สามารถอัปเดตการตั้งค่าการแชร์เซสชันได้", "sessionTimeout": "หมดเวลาเซสชัน", "hours": "ชั่วโมง", "sessionTimeoutRange": "ขั้นต่ำ 1 ชั่วโมง · ขั้นสูงสุด 720 ชั่วโมง", diff --git a/src/ui/locales/translated/tr_TR.json b/src/ui/locales/translated/tr_TR.json index fbf00371f..6f0e33c71 100644 --- a/src/ui/locales/translated/tr_TR.json +++ b/src/ui/locales/translated/tr_TR.json @@ -667,7 +667,6 @@ "done_cli_link": "CLI belgelerini açın" }, "nav": { - "collab": "Toplantılar", "home": "Ana Sayfa", "terminal": "Terminal", "localTerminal": "Yerel Terminal", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Kabuk uygulamanızı ve içinde çalışan her şeyi bağlantı kesintileri ve tarayıcı yeniden başlatmaları sırasında çalışır durumda tutmak için tmux içinde çalıştırın (tmux ana bilgisayarda kurulu olmalıdır).", "enableSessionLogging": "Oturum Kaydı", "enableSessionLoggingDesc": "Daha sonra gözden geçirmek için terminal oturumu çıktısını kaydet", - "allowSessionSharing": "Oturum Paylaşımına İzin Ver", - "allowSessionSharingDesc": "Bu sunucudaki canlı oturumların bağlantı yoluyla veya diğer kullanıcılarla paylaşılmasına izin verin.", "localEchoLabel": "Yerel Yankı", "localEchoDesc": "Yavaş bağlantılarda bile yerel olarak güvenli yazdırılabilir tuş vuruşlarını tahmin edin. Parola istemleri ve kontrol girişleri asla tekrarlanmaz.", "localEchoDefault": "Genel ayarı kullanın", @@ -1571,137 +1568,11 @@ "join": "Katılmak", "sharedSessionLabel": "{{hostName}} (paylaşılan)" }, - "collab": { - "createRoom": "Yeni oda", - "roomName": "Oda adı", - "persistentRoom": "Kalıcı oda", - "persistentRoomHint": "Kalıcı olarak tahsis edilen odalar, toplantı bittikten sonra da listede kalır ve tekrar kullanılabilir.", - "noRooms": "Henüz oda yok. Toplantı başlatmak için bir oda oluşturun.", - "openRoom": "Açık", - "endRoom": "Toplantıyı sonlandırın", - "leaveRoom": "Ayrılmak", - "deleteMember": "Kaldırmak", - "removeMember": "{{name}}'ı kaldır", - "removeMemberTitle": "Bu üyeyi kaldırmak mı istiyorsunuz?", - "removeMemberDescription": "{{name}} bu odaya erişimini derhal kaybedecektir. Aktif olan tüm SSH izleyicilerinin bağlantısı kesilecektir.", - "invite": "Davet etmek", - "inviteTitle": "Üyeleri davet edin", - "members": "Üyeler", - "membersPanel": "Oda üyeleri ve kontrol talepleri", - "membersWithCount": "Üyeler ({{count}})", - "online": "çevrimiçi", - "offline": "çevrimdışı", - "you": "(Sen)", - "hostBadge": "Ev sahibi", - "presenterBadge": "Sunar", - "present": "Sunmak", - "presentTitle": "Sahneye çıkın", - "presentHost": "Ev sahibi", - "searchHosts": "Ev sahiplerini ara", - "noHostsFound": "Kullanılabilir sunucu bulunamadı.", - "presentProtocol": "Protokol", - "stopPresenting": "Sunum yapmayı bırakın", - "takeOver": "Devralmak", - "takeOverConfirmTitle": "Mevcut sunumu değiştirmek mi istiyorsunuz?", - "takeOverDescription": "{{name}} şu anda sunum yapıyor. Devralmak, herkesi o sahneden ayıracaktır.", - "emptyStage": "Kimse sunum yapmıyor. Sahneye çıkın ve bir sunum paylaşın.", - "stageLoading": "Sahneye bağlanmak...", - "stageEnded": "Sunum sona erdi.", - "roomEnded": "Bu toplantı sona erdi.", - "presenterLabel": "{{name}} sunuyor", - "guestLink": "Konuk bağlantısı", - "guestLinkOn": "Misafir bağlantısı aktif. Bağlantıya sahip herkes sahneyi izleyebilir.", - "guestLinkOff": "Misafir bağlantısı kapalı.", - "copyLink": "Bağlantıyı kopyala", - "rotateLink": "Bağlantıyı döndür", - "disableLink": "Bağlantıyı devre dışı bırak", - "rotateLinkConfirmTitle": "Konuk bağlantısını döndürelim mi?", - "rotateLinkDescription": "Mevcut bağlantı derhal çalışmayı durduracaktır. Bağlı SSH misafirlerinin bağlantısı kesilecektir.", - "disableLinkConfirmTitle": "Misafir bağlantısını devre dışı bırak?", - "disableLinkDescription": "Anonim SSH izleyicilerinin bağlantısı derhal kesilecek ve mevcut bağlantı çalışmayı durduracaktır.", - "linkCopied": "Bağlantı kopyalandı", - "roles": "Roller", - "users": "Kullanıcılar", - "searchUsers": "Kullanıcıları ara", - "endConfirmTitle": "Bu toplantıyı sonlandıralım mı?", - "endDescription": "{{name}} herkes için sona erecek ve yeniden açılamayacak.", - "endPersistentDescription": "{{name}} 'daki aktif sunum durdurulacaktır. Oda yeniden kullanıma açıktır.", - "deleteRoom": "Odayı sil", - "deleteConfirmTitle": "Bu odayı silmek mi?", - "deleteDescription": "{{name}} herkes için kalıcı olarak silinecek ve geri alınamaz.", - "invitedTo": "\"{{name}} \" etkinliğine davet edildiniz.", - "guest": { - "title": "Toplantı", - "waiting": "Sunucuyu bekliyoruz...", - "linkInvalid": "Bu konuk bağlantısı geçersiz veya toplantı sona erdi." - }, - "reopenFromPanel": "Bu toplantı sekmesinin süresi doldu. Toplantılar panelinden odayı yeniden açın.", - "requestControl": "Talep kontrolü", - "cancelControlRequest": "İptal talebi", - "controlQueue": "Kontrol talepleri", - "noControlRequests": "Bekleyen talep yok.", - "grantControlTo": "Kontrolü {{name}}'a verin.", - "dismissControlRequest": "{{name}}'ın kontrol isteğini reddet", - "releaseControl": "Kontrolü serbest bırak", - "grantControl": "Kontrolü ona verin.", - "revokeControl": "Kontrolü geri alın", - "controlRequestedBy": "{{name}} kontrolü istedi", - "grant": "Hibe etmek", - "controlBadge": "Kontrol altında", - "youArePresenting": "Başka bir sekmede sunum yapıyorsunuz. Sunumu durdurun ve sahneyi karşı tarafa devredin.", - "created": "Oda oluşturuldu", - "invited": "Üyeler davet edildi", - "roomTab": "Toplantı" - }, "sessionSharing": { - "guestView": { - "loading": "Paylaşılan oturuma bağlanılıyor...", - "linkInvalid": "Bu paylaşım bağlantısı geçersiz, süresi dolmuş veya iptal edilmiş.", - "rateLimited": "Çok fazla deneme yapıldı, lütfen kısa süre sonra tekrar deneyin.", - "sessionEnded": "Bu oturum sona erdi.", - "readOnlyBadge": "Yalnızca görüntüle", - "ownerLabel": "Ev sahibi" - }, - "modalTitle": "Oturumu paylaş", - "shareButton": "Paylaşmak", - "notReadyToShare": "Oturum henüz paylaşılmaya hazır değil.", - "modeTab": { - "link": "Bağlantı", - "user": "Kullanıcı" - }, - "linkModeDescription": "Bu bağlantıya sahip herkes katılabilir, hesap gerekmez.", - "userModeDescription": "Bu sunucuya zaten erişimi olan belirli bir kullanıcıyla paylaşın. Henüz erişimleri yoksa, önce sunucuyu onlarla paylaşın veya bunun yerine bir bağlantı kullanın. Paylaşıldıktan sonra, oturum Bağlantılar sekmesinde görünür.", "permissionLevel": { - "label": "İzin seviyesi", "readOnly": "salt okunur", - "readOnlyDescription": "Oturumu canlı olarak izleyebilirsiniz ancak yazı yazamaz veya etkileşimde bulunamazsınız.", - "readWrite": "Okuma-yazma", - "readWriteDescription": "Oturum sahibi gibi yazabilir ve oturumla etkileşim kurabilirsiniz." - }, - "expiryLabel": "Bağlantı süresinin dolması", - "createLinkButton": "Bağlantı oluştur", - "createShareButton": "Kullanıcıyla paylaş", - "searchUsersPlaceholder": "Kullanıcıları ara...", - "noUsersFound": "Hiç kullanıcı bulunamadı.", - "linkCreated": "Paylaşım bağlantısı oluşturuldu.", - "linkCopied": "Bağlantı panoya kopyalandı", - "copyLink": "Bağlantıyı kopyala", - "shareCreated": "Oturum paylaşıldı. Bu, onların Bağlantılar sekmesinde görünecektir.", - "shareFailed": "Paylaşım oluşturulamadı.", - "userLacksHostAccess": "Bu kullanıcının henüz bu sunucuya erişimi yok. Öncelikle sunucuyu onlarla paylaşın veya bunun yerine bir bağlantı kullanın.", - "activeShares": "Aktif hisseler", - "noActiveShares": "Bu oturum için aktif hisse senedi bulunmamaktadır.", - "revoke": "Geri çekmek", - "revokeConfirmTitle": "Bu hisseyi iptal etmek ister misiniz?", - "revokeConfirmDescription": "Bu paylaşımı kullanan herkes erişimini anında kaybedecektir.", - "revoked": "Hisseler geri alındı", - "revokeFailed": "Hisseleri geri alma işlemi başarısız oldu.", - "joinCount": "{{count}} katıl", - "joinCount_other": "{{count}} birleşiyor", - "expiresAt": "Süresi doluyor {{date}}", - "linkShareBadge": "Bağlantı", - "userShareBadge": "Kullanıcı: {{username}}", - "loadSharesFailed": "Aktif paylaşımlar yüklenemedi." + "readWrite": "Okuma-yazma" + } }, "terminal": { "connect": "Sunucuya Bağlan", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Termix'i geliştirmeye yardımcı olmak için kullanıcı, sunucu ve özellik kullanımına ilişkin anonim günlük sayım gönderir. Hiçbir kişisel veri veya bağlantı bilgisi asla dahil edilmez.", "analyticsEnabledLockedDesc": "Bu ayar, ENABLE_TELEMETRY ortam değişkeni tarafından kilitlenmiştir ve burada değiştirilemez.", "updateAnalyticsFailed": "Analiz ayarlarını güncelleme başarısız oldu.", - "sessionSharingGloballyEnabled": "Oturum Paylaşımına İzin Ver", "auditForwardingTitle": "SIEM'e ilet", "auditForwardingDesc": "Her denetim kaydının bir kopyasını NDJSON formatında harici bir toplayıcıya gönderin. Yerel günlükler, doğruluk kaynağı olmaya devam eder.", "auditForwardingEnvNotice": "Ortam değişkeni de yönlendirmeyi yapılandırır; burada kaydedilen URL önceliklidir.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Taşıyıcı belirteç (saklanmış)", "auditForwardingSaved": "Denetim yönlendirme ayarları kaydedildi.", "auditForwardingSaveError": "Denetim yönlendirme ayarları kaydedilemedi.", - "sessionSharingGloballyEnabledDesc": "Canlı terminal, RDP, VNC ve Telnet oturumlarının tüm örnek genelinde paylaşılmasına izin verin. Devre dışı bırakıldığında, sunucu başına paylaşım ayarlarının tümünü geçersiz kılar.", - "updateSessionSharingFailed": "Oturum paylaşım ayarı güncellenemedi.", "sessionTimeout": "Oturum Zaman Aşımı", "hours": "saat", "sessionTimeoutRange": "En az 1 saat · En fazla 720 saat", diff --git a/src/ui/locales/translated/uk_UA.json b/src/ui/locales/translated/uk_UA.json index 13beb3556..9907fab86 100644 --- a/src/ui/locales/translated/uk_UA.json +++ b/src/ui/locales/translated/uk_UA.json @@ -667,7 +667,6 @@ "done_cli_link": "Відкрийте документацію CLI" }, "nav": { - "collab": "Зустрічі", "home": "Дім", "terminal": "Термінал", "localTerminal": "Місцевий термінал", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Зберігайте вашу оболонку та все, що в ній працює, активними після відключень та перезапусків браузера, запускаючи її всередині tmux (tmux має бути встановлений на хості)", "enableSessionLogging": "Ведення журналу сеансу", "enableSessionLoggingDesc": "Запис виводу сеансу терміналу для подальшого перегляду", - "allowSessionSharing": "Дозволити спільний доступ до сеансу", - "allowSessionSharingDesc": "Дозволити ділитися прямими трансляціями на цьому хості через посилання або з іншими користувачами", "localEchoLabel": "Локальне відлуння", "localEchoDesc": "Передбачати безпечні для друку натискання клавіш локально на повільних з'єднаннях. Запити на введення пароля та введені елементи керування ніколи не відображаються.", "localEchoDefault": "Використовувати глобальні налаштування", @@ -1571,137 +1568,11 @@ "join": "Приєднатися", "sharedSessionLabel": "{{hostName}} (спільний доступ)" }, - "collab": { - "createRoom": "Нова кімната", - "roomName": "Назва кімнати", - "persistentRoom": "Постійна кімната", - "persistentRoomHint": "Постійні кімнати залишаються у списку після завершення зустрічі та можуть бути використані повторно.", - "noRooms": "Поки що немає кімнат. Створіть одну, щоб розпочати зустріч.", - "openRoom": "ВІДЧИНЕНО", - "endRoom": "Закінчити зустріч", - "leaveRoom": "Залишити", - "deleteMember": "Видалити", - "removeMember": "Видалити {{name}}", - "removeMemberTitle": "Видалити цього учасника?", - "removeMemberDescription": "{{name}} негайно втратить доступ до цієї кімнати. Будь-який активний переглядач SSH буде відключено.", - "invite": "Запросити", - "inviteTitle": "Запросити учасників", - "members": "Члени", - "membersPanel": "Учасники кімнати та запити на керування", - "membersWithCount": "Учасники ({{count}})", - "online": "онлайн", - "offline": "офлайн", - "you": "(ти)", - "hostBadge": "Хост", - "presenterBadge": "Презентація", - "present": "Подарунок", - "presentTitle": "Вийди на сцену", - "presentHost": "Хост", - "searchHosts": "Пошук хостів", - "noHostsFound": "Не знайдено доступних хостів.", - "presentProtocol": "Протокол", - "stopPresenting": "Зупинити презентацію", - "takeOver": "Захопити", - "takeOverConfirmTitle": "Замінити поточну презентацію?", - "takeOverDescription": "{{name}} зараз презентує. Перехоплення сцени відключить усіх учасників.", - "emptyStage": "Ніхто не виступає. Вийдіть на сцену, щоб поділитися сесією.", - "stageLoading": "Підключення до сцени...", - "stageEnded": "Презентація закінчилася", - "roomEnded": "Ця зустріч завершилася", - "presenterLabel": "{{name}} презентує", - "guestLink": "Гостьове посилання", - "guestLinkOn": "Гостьове посилання ввімкнено. Будь-хто, хто має посилання, може дивитися сцену.", - "guestLinkOff": "Гостьове посилання вимкнено.", - "copyLink": "Копіювати посилання", - "rotateLink": "Повернути посилання", - "disableLink": "Вимкнути посилання", - "rotateLinkConfirmTitle": "Повернути гостьове посилання?", - "rotateLinkDescription": "Поточне посилання негайно перестане працювати. Підключені гості SSH будуть відключені.", - "disableLinkConfirmTitle": "Вимкнути гостьове посилання?", - "disableLinkDescription": "Анонімні користувачі SSH будуть негайно відключені, а поточне посилання перестане працювати.", - "linkCopied": "Посилання скопійовано", - "roles": "Ролі", - "users": "Користувачі", - "searchUsers": "Пошук користувачів", - "endConfirmTitle": "Завершити цю зустріч?", - "endDescription": "{{name}} завершиться для всіх і не може бути відкрито повторно.", - "endPersistentDescription": "Активна презентація в {{name}} буде зупинена. Кімната залишається доступною для повторного використання.", - "deleteRoom": "Видалити кімнату", - "deleteConfirmTitle": "Видалити цю кімнату?", - "deleteDescription": "{{name}} буде видалено назавжди для всіх і цю дію не можна буде скасувати.", - "invitedTo": "Вас запросили до «{{name}}»", - "guest": { - "title": "Зустріч", - "waiting": "Чекаємо на ведучого...", - "linkInvalid": "Це гостьове посилання недійсне або зустріч завершилася" - }, - "reopenFromPanel": "Термін дії цієї вкладки зустрічі закінчився. Відкрийте кімнату знову на панелі Зустрічі.", - "requestControl": "Запит на керування", - "cancelControlRequest": "Скасувати запит", - "controlQueue": "Запити на керування", - "noControlRequests": "Немає запитів, що очікують на розгляд.", - "grantControlTo": "Надайте керування {{name}}", - "dismissControlRequest": "Відхилити запит керування від {{name}}", - "releaseControl": "Контроль випуску", - "grantControl": "Надайте контроль", - "revokeControl": "Поверніть собі контроль", - "controlRequestedBy": "{{name}} запросив керування", - "grant": "Грант", - "controlBadge": "Контроль", - "youArePresenting": "Ви презентуєте в іншій вкладці. Зупиніть презентацію, щоб передати сцену.", - "created": "Кімнату створено", - "invited": "Запрошені учасники", - "roomTab": "Зустріч" - }, "sessionSharing": { - "guestView": { - "loading": "Підключення до спільного сеансу...", - "linkInvalid": "Це посилання для спільного доступу недійсне, термін його дії минув або його було відкликано", - "rateLimited": "Забагато спроб, спробуйте ще раз пізніше", - "sessionEnded": "Цей сеанс завершився", - "readOnlyBadge": "Тільки перегляд", - "ownerLabel": "Власник хоста" - }, - "modalTitle": "Поділитися сеансом", - "shareButton": "Поділитися", - "notReadyToShare": "Сеанс ще не готовий до поширення", - "modeTab": { - "link": "Посилання", - "user": "Користувач" - }, - "linkModeDescription": "Приєднатися може кожен, хто має це посилання, обліковий запис не потрібен.", - "userModeDescription": "Надайте доступ певному користувачеві, який уже має доступ до цього хоста. Якщо у нього ще немає доступу, спочатку надайте йому доступ до хоста або скористайтеся посиланням. Після надання доступу сеанс відображається на його вкладці «Підключення».", "permissionLevel": { - "label": "Рівень дозволів", "readOnly": "Тільки для читання", - "readOnlyDescription": "Можна дивитися сеанс у прямому ефірі, але не можна друкувати чи взаємодіяти.", - "readWrite": "Читання-запис", - "readWriteDescription": "Можна друкувати та взаємодіяти з сеансом так само, як і його власник." - }, - "expiryLabel": "Термін дії посилання", - "createLinkButton": "Створити посилання", - "createShareButton": "Поділитися з користувачем", - "searchUsersPlaceholder": "Пошук користувачів...", - "noUsersFound": "Користувачів не знайдено", - "linkCreated": "Посилання для спільного доступу створено", - "linkCopied": "Посилання скопійовано в буфер обміну", - "copyLink": "Копіювати посилання", - "shareCreated": "Сеанс спільний. Він відображатиметься на вкладці «Зв’язки».", - "shareFailed": "Не вдалося створити спільний доступ", - "userLacksHostAccess": "Цей користувач ще не має доступу до цього хоста. Спочатку надайте йому доступ до хоста або скористайтеся посиланням.", - "activeShares": "Активні акції", - "noActiveShares": "Немає активних поширень для цього сеансу", - "revoke": "Скасувати", - "revokeConfirmTitle": "Скасувати цей спільний доступ?", - "revokeConfirmDescription": "Будь-хто, хто користується цим ресурсом спільного доступу, негайно втратить доступ.", - "revoked": "Акції анульовано", - "revokeFailed": "Не вдалося скасувати спільний доступ", - "joinCount": "{{count}} приєднатися", - "joinCount_other": "{{count}} приєднується", - "expiresAt": "Термін дії закінчується {{date}}", - "linkShareBadge": "Посилання", - "userShareBadge": "Користувач: {{username}}", - "loadSharesFailed": "Не вдалося завантажити активні спільні ресурси" + "readWrite": "Читання-запис" + } }, "terminal": { "connect": "Підключення до хоста", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Надсилає анонімний щоденний підрахунок користувачів, хостів та використання функцій, щоб допомогти покращити Termix. Жодні особисті дані чи деталі з’єднання ніколи не включаються.", "analyticsEnabledLockedDesc": "Цей параметр заблоковано змінною середовища ENABLE_TELEMETRY і його не можна змінити тут.", "updateAnalyticsFailed": "Не вдалося оновити налаштування аналітики", - "sessionSharingGloballyEnabled": "Дозволити спільний доступ до сеансу", "auditForwardingTitle": "Вперед до SIEM", "auditForwardingDesc": "Надсилайте копію кожного запису аудиту зовнішньому колектору як NDJSON. Локальні журнали залишаються джерелом достовірної інформації.", "auditForwardingEnvNotice": "Змінна середовища також налаштовує переадресацію; URL-адреса, збережена тут, має пріоритет.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Токен пред'явника (зберігається)", "auditForwardingSaved": "Налаштування переадресації аудиту збережено", "auditForwardingSaveError": "Не вдалося зберегти налаштування переадресації аудиту", - "sessionSharingGloballyEnabledDesc": "Дозволити спільний доступ до сеансів терміналу, RDP, VNC та Telnet для всього екземпляра. Перевизначає кожен перемикач спільного доступу для кожного хоста, якщо його вимкнено.", - "updateSessionSharingFailed": "Не вдалося оновити налаштування спільного доступу до сеансу", "sessionTimeout": "Час очікування сеансу", "hours": "години", "sessionTimeoutRange": "Мін. 1 год · Макс. 720 год", diff --git a/src/ui/locales/translated/vi_VN.json b/src/ui/locales/translated/vi_VN.json index f2db13bb3..f45ad6507 100644 --- a/src/ui/locales/translated/vi_VN.json +++ b/src/ui/locales/translated/vi_VN.json @@ -667,7 +667,6 @@ "done_cli_link": "Mở tài liệu CLI" }, "nav": { - "collab": "Các cuộc họp", "home": "Trang chủ", "terminal": "Terminal", "localTerminal": "Ga địa phương", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "Giữ cho shell và mọi thứ đang chạy trong đó hoạt động bình thường ngay cả khi bị ngắt kết nối hoặc khởi động lại trình duyệt bằng cách chạy bên trong tmux (tmux phải được cài đặt trên máy chủ).", "enableSessionLogging": "Ghi nhật ký phiên", "enableSessionLoggingDesc": "Ghi lại đầu ra phiên terminal để xem lại sau", - "allowSessionSharing": "Cho phép chia sẻ phiên", - "allowSessionSharingDesc": "Cho phép chia sẻ các phiên trực tiếp trên máy chủ này qua liên kết hoặc với những người dùng khác.", "localEchoLabel": "Tiếng vọng địa phương", "localEchoDesc": "Dự đoán các thao tác gõ phím an toàn có thể in được cục bộ trên các kết nối chậm. Lời nhắc nhập mật khẩu và đầu vào điều khiển không bao giờ được hiển thị lại.", "localEchoDefault": "Sử dụng cài đặt toàn cầu", @@ -1571,137 +1568,11 @@ "join": "Tham gia", "sharedSessionLabel": "{{hostName}} (được chia sẻ)" }, - "collab": { - "createRoom": "Phòng mới", - "roomName": "Tên phòng", - "persistentRoom": "Phòng bền vững", - "persistentRoomHint": "Các phòng họp được lưu lại sẽ vẫn hiển thị sau khi cuộc họp kết thúc và có thể được sử dụng lại.", - "noRooms": "Chưa có phòng nào. Hãy tạo một phòng để bắt đầu cuộc họp.", - "openRoom": "Mở", - "endRoom": "Kết thúc cuộc họp", - "leaveRoom": "Rời khỏi", - "deleteMember": "Di dời", - "removeMember": "Xóa {{name}}", - "removeMemberTitle": "Xóa thành viên này?", - "removeMemberDescription": "{{name}} sẽ mất quyền truy cập vào phòng này ngay lập tức. Bất kỳ trình xem SSH nào đang hoạt động sẽ bị ngắt kết nối.", - "invite": "Mời", - "inviteTitle": "Mời thành viên", - "members": "Thành viên", - "membersPanel": "Thành viên phòng và yêu cầu kiểm soát", - "membersWithCount": "Thành viên ({{count}})", - "online": "trực tuyến", - "offline": "ngoại tuyến", - "you": "(Bạn)", - "hostBadge": "Chủ nhà", - "presenterBadge": "Giới thiệu", - "present": "Hiện tại", - "presentTitle": "Hãy lên sân khấu", - "presentHost": "Chủ nhà", - "searchHosts": "Tìm kiếm máy chủ", - "noHostsFound": "Không tìm thấy máy chủ nào khả dụng.", - "presentProtocol": "Giao thức", - "stopPresenting": "Dừng thuyết trình", - "takeOver": "Tiếp quản", - "takeOverConfirmTitle": "Bạn muốn thay thế bài thuyết trình hiện tại?", - "takeOverDescription": "{{name}} đang trình bày. Việc tiếp quản sẽ ngắt kết nối mọi người khỏi sân khấu đó.", - "emptyStage": "Hiện chưa có ai thuyết trình. Hãy lên sân khấu để chia sẻ một phiên thảo luận.", - "stageLoading": "Kết nối với sân khấu...", - "stageEnded": "Buổi thuyết trình kết thúc.", - "roomEnded": "Cuộc họp này đã kết thúc.", - "presenterLabel": "{{name}} đang trình bày", - "guestLink": "Liên kết khách", - "guestLinkOn": "Link dành cho khách đã được bật. Bất cứ ai có link đều có thể xem chương trình.", - "guestLinkOff": "Liên kết khách đã bị tắt.", - "copyLink": "Sao chép liên kết", - "rotateLink": "Xoay liên kết", - "disableLink": "Vô hiệu hóa liên kết", - "rotateLinkConfirmTitle": "Xoay liên kết khách?", - "rotateLinkDescription": "Kết nối hiện tại sẽ ngừng hoạt động ngay lập tức. Các máy khách SSH đang kết nối sẽ bị ngắt kết nối.", - "disableLinkConfirmTitle": "Vô hiệu hóa liên kết khách?", - "disableLinkDescription": "Những người xem SSH ẩn danh sẽ bị ngắt kết nối ngay lập tức và liên kết hiện tại sẽ ngừng hoạt động.", - "linkCopied": "Liên kết đã được sao chép", - "roles": "Vai trò", - "users": "Người dùng", - "searchUsers": "Tìm kiếm người dùng", - "endConfirmTitle": "Kết thúc cuộc họp này?", - "endDescription": "{{name}} sẽ kết thúc đối với tất cả mọi người và không thể mở lại.", - "endPersistentDescription": "Buổi thuyết trình đang diễn ra tại {{name}} sẽ dừng lại. Phòng vẫn có thể được sử dụng lại.", - "deleteRoom": "Xóa phòng", - "deleteConfirmTitle": "Xóa phòng này?", - "deleteDescription": "{{name}} sẽ bị xóa vĩnh viễn đối với tất cả mọi người và không thể hoàn tác.", - "invitedTo": "Bạn được mời đến \"{{name}}\"", - "guest": { - "title": "Cuộc họp", - "waiting": "Đang chờ người dẫn chương trình...", - "linkInvalid": "Liên kết dành cho khách này không hợp lệ hoặc cuộc họp đã kết thúc." - }, - "reopenFromPanel": "Tab cuộc họp này đã hết hạn. Vui lòng mở lại phòng từ mục Cuộc họp.", - "requestControl": "Yêu cầu kiểm soát", - "cancelControlRequest": "Yêu cầu hủy bỏ", - "controlQueue": "Yêu cầu kiểm soát", - "noControlRequests": "Không có yêu cầu nào đang chờ xử lý.", - "grantControlTo": "Trao quyền điều khiển cho {{name}}", - "dismissControlRequest": "Bỏ qua yêu cầu điều khiển của {{name}}", - "releaseControl": "Kiểm soát giải phóng", - "grantControl": "Trao quyền kiểm soát", - "revokeControl": "Giành lại quyền kiểm soát", - "controlRequestedBy": "{{name}} yêu cầu quyền điều khiển", - "grant": "Khoản trợ cấp", - "controlBadge": "Trong tầm kiểm soát", - "youArePresenting": "Bạn đang thuyết trình ở một tab khác. Hãy dừng thuyết trình để nhường sân khấu.", - "created": "Phòng được tạo", - "invited": "Các thành viên được mời", - "roomTab": "Cuộc họp" - }, "sessionSharing": { - "guestView": { - "loading": "Đang kết nối đến phiên chia sẻ...", - "linkInvalid": "Liên kết chia sẻ này không hợp lệ, đã hết hạn hoặc đã bị thu hồi.", - "rateLimited": "Đã thử quá nhiều lần, vui lòng thử lại sau.", - "sessionEnded": "Phiên này đã kết thúc", - "readOnlyBadge": "Chỉ xem", - "ownerLabel": "Chủ nhà" - }, - "modalTitle": "Chia sẻ phiên họp", - "shareButton": "Chia sẻ", - "notReadyToShare": "Buổi học chưa sẵn sàng để chia sẻ.", - "modeTab": { - "link": "Liên kết", - "user": "Người dùng" - }, - "linkModeDescription": "Bất kỳ ai có liên kết này đều có thể tham gia, không cần tài khoản.", - "userModeDescription": "Chia sẻ với một người dùng cụ thể đã có quyền truy cập vào máy chủ này. Nếu họ chưa có quyền truy cập, hãy chia sẻ máy chủ với họ trước hoặc sử dụng liên kết thay thế. Sau khi chia sẻ, phiên làm việc sẽ xuất hiện trong tab Kết nối của họ.", "permissionLevel": { - "label": "Cấp độ quyền hạn", "readOnly": "Chỉ đọc", - "readOnlyDescription": "Có thể xem phiên trực tiếp nhưng không thể gõ chữ hoặc tương tác.", - "readWrite": "Đọc-viết", - "readWriteDescription": "Có thể nhập liệu và tương tác với phiên làm việc giống như chủ sở hữu." - }, - "expiryLabel": "Liên kết hết hạn", - "createLinkButton": "Tạo liên kết", - "createShareButton": "Chia sẻ với người dùng", - "searchUsersPlaceholder": "Tìm kiếm người dùng...", - "noUsersFound": "Không tìm thấy người dùng nào", - "linkCreated": "Đã tạo liên kết chia sẻ", - "linkCopied": "Liên kết đã được sao chép vào clipboard", - "copyLink": "Sao chép liên kết", - "shareCreated": "Phiên họp đã được chia sẻ. Nó sẽ xuất hiện trong tab Kết nối của họ.", - "shareFailed": "Không thể tạo chia sẻ", - "userLacksHostAccess": "Người dùng đó chưa có quyền truy cập vào máy chủ này. Hãy chia sẻ thông tin máy chủ với họ trước, hoặc sử dụng liên kết thay thế.", - "activeShares": "Cổ phiếu tích cực", - "noActiveShares": "Hiện không có cổ phiếu nào đang được giao dịch trong phiên này.", - "revoke": "Thu hồi", - "revokeConfirmTitle": "Thu hồi cổ phần này?", - "revokeConfirmDescription": "Bất kỳ ai sử dụng thư mục chia sẻ này sẽ mất quyền truy cập ngay lập tức.", - "revoked": "Cổ phần đã bị thu hồi", - "revokeFailed": "Không thể thu hồi cổ phần", - "joinCount": "{{count}} tham gia", - "joinCount_other": "{{count}} tham gia", - "expiresAt": "Hết hạn {{date}}", - "linkShareBadge": "Liên kết", - "userShareBadge": "Người dùng: {{username}}", - "loadSharesFailed": "Không thể tải các chia sẻ đang hoạt động" + "readWrite": "Đọc-viết" + } }, "terminal": { "connect": "Kết nối đến máy chủ", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "Gửi báo cáo hàng ngày ẩn danh về số lượng người dùng, máy chủ và mức sử dụng tính năng để giúp cải thiện Termix. Không có dữ liệu cá nhân hoặc chi tiết kết nối nào được tiết lộ.", "analyticsEnabledLockedDesc": "Cài đặt này bị khóa bởi biến môi trường ENABLE_TELEMETRY và không thể thay đổi ở đây.", "updateAnalyticsFailed": "Không thể cập nhật cài đặt phân tích.", - "sessionSharingGloballyEnabled": "Cho phép chia sẻ phiên", "auditForwardingTitle": "Chuyển tiếp đến SIEM", "auditForwardingDesc": "Gửi một bản sao của mỗi mục kiểm toán đến một đơn vị thu thập dữ liệu bên ngoài dưới dạng NDJSON. Nhật ký cục bộ vẫn là nguồn thông tin chính xác.", "auditForwardingEnvNotice": "Một biến môi trường cũng cấu hình việc chuyển tiếp; URL được lưu ở đây sẽ được ưu tiên.", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "Mã thông báo người mang (đã lưu trữ)", "auditForwardingSaved": "Cài đặt chuyển tiếp kiểm toán đã được lưu", "auditForwardingSaveError": "Không thể lưu cài đặt chuyển tiếp kiểm toán.", - "sessionSharingGloballyEnabledDesc": "Cho phép chia sẻ các phiên terminal trực tiếp, RDP, VNC và Telnet trên toàn bộ máy chủ. Khi bị vô hiệu hóa, tùy chọn này sẽ ghi đè lên mọi tùy chọn chia sẻ trên từng máy chủ riêng lẻ.", - "updateSessionSharingFailed": "Không thể cập nhật cài đặt chia sẻ phiên.", "sessionTimeout": "Thời gian chờ phiên", "hours": "giờ", "sessionTimeoutRange": "Tối thiểu 1h · Tối đa 720h", diff --git a/src/ui/locales/translated/zh_CN.json b/src/ui/locales/translated/zh_CN.json index bc4185ed4..40a2242fd 100644 --- a/src/ui/locales/translated/zh_CN.json +++ b/src/ui/locales/translated/zh_CN.json @@ -667,7 +667,6 @@ "done_cli_link": "打开 CLI 文档" }, "nav": { - "collab": "会议", "home": "首页", "terminal": "终端", "localTerminal": "本地终端", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "通过在 tmux 中运行(tmux 必须安装在主机上),可以在断开连接和浏览器重启后保持 shell 及其中运行的所有内容处于活动状态。", "enableSessionLogging": "会话日志记录", "enableSessionLoggingDesc": "记录终端会话输出以供日后查看", - "allowSessionSharing": "允许会话共享", - "allowSessionSharingDesc": "允许通过链接或其他方式共享此主机上的实时会话。", "localEchoLabel": "本地回声", "localEchoDesc": "在连接速度较慢的情况下,本地预测可安全打印的按键操作。密码提示和控制输入永远不会被回显。", "localEchoDefault": "使用全局设置", @@ -1571,137 +1568,11 @@ "join": "加入", "sharedSessionLabel": "{{hostName}}(共享)" }, - "collab": { - "createRoom": "新房间", - "roomName": "房间名称", - "persistentRoom": "持久房间", - "persistentRoomHint": "会议结束后,持久会议室仍会保留在列表中,可以重复使用。", - "noRooms": "暂无会议室。创建会议室即可开始会议。", - "openRoom": "打开", - "endRoom": "会议结束", - "leaveRoom": "离开", - "deleteMember": "消除", - "removeMember": "移除 {{name}}", - "removeMemberTitle": "移除此成员?", - "removeMemberDescription": "{{name}} 将立即失去对该房间的访问权限。所有正在使用的 SSH 查看器都将被断开连接。", - "invite": "邀请", - "inviteTitle": "邀请成员", - "members": "成员", - "membersPanel": "房间成员和控制请求", - "membersWithCount": "成员({{count}})", - "online": "在线的", - "offline": "离线", - "you": "(你)", - "hostBadge": "主持人", - "presenterBadge": "呈现", - "present": "展示", - "presentTitle": "登上舞台", - "presentHost": "主持人", - "searchHosts": "搜索主机", - "noHostsFound": "未找到可用主机。", - "presentProtocol": "协议", - "stopPresenting": "停止演示", - "takeOver": "接管", - "takeOverConfirmTitle": "替换当前演示文稿?", - "takeOverDescription": "{{name}} 正在直播。接管直播将会断开所有人与该直播间的连接。", - "emptyStage": "目前无人发言。请上台分享您的演讲内容。", - "stageLoading": "连接到舞台……", - "stageEnded": "演讲结束了", - "roomEnded": "会议结束", - "presenterLabel": "{{name}} 正在呈现", - "guestLink": "访客链接", - "guestLinkOn": "访客链接已开启。任何拥有该链接的人都可以观看舞台表演。", - "guestLinkOff": "访客链接已关闭。", - "copyLink": "复制链接", - "rotateLink": "旋转链接", - "disableLink": "禁用链接", - "rotateLinkConfirmTitle": "旋转访客链接?", - "rotateLinkDescription": "当前连接将立即停止工作。已连接的SSH访客将被断开连接。", - "disableLinkConfirmTitle": "禁用访客链接?", - "disableLinkDescription": "匿名 SSH 查看者将被立即断开连接,当前链接将停止工作。", - "linkCopied": "链接已复制", - "roles": "角色", - "users": "用户", - "searchUsers": "搜索用户", - "endConfirmTitle": "结束会议吗?", - "endDescription": "{{name}} 将对所有人关闭,且无法重新开启。", - "endPersistentDescription": "{{name}} 中的当前演示将停止。该房间仍可供再次使用。", - "deleteRoom": "删除房间", - "deleteConfirmTitle": "删除这个房间?", - "deleteDescription": "{{name}} 将被永久删除,且无法撤销。", - "invitedTo": "您受邀参加“{{name}}”", - "guest": { - "title": "会议", - "waiting": "等待主持人……", - "linkInvalid": "此访客链接无效或会议已结束" - }, - "reopenFromPanel": "此会议标签页已过期。请从“会议”面板重新打开会议室。", - "requestControl": "请求控制", - "cancelControlRequest": "取消请求", - "controlQueue": "控制请求", - "noControlRequests": "没有待处理的请求。", - "grantControlTo": "将控制权交给 {{name}}", - "dismissControlRequest": "驳回 {{name}}的控制请求", - "releaseControl": "发布控制", - "grantControl": "赋予控制权", - "revokeControl": "夺回控制权", - "controlRequestedBy": "{{name}} 请求控制权", - "grant": "授予", - "controlBadge": "掌控之中", - "youArePresenting": "您正在另一个标签页中进行演示。请停止演示,将演示权交给对方。", - "created": "创建房间", - "invited": "受邀成员", - "roomTab": "会议" - }, "sessionSharing": { - "guestView": { - "loading": "正在连接到共享会话...", - "linkInvalid": "此共享链接无效、已过期或已被撤销。", - "rateLimited": "尝试次数过多,请稍后再试。", - "sessionEnded": "此会话已结束", - "readOnlyBadge": "仅查看", - "ownerLabel": "房东" - }, - "modalTitle": "共享会话", - "shareButton": "共享", - "notReadyToShare": "会话尚未准备好共享", - "modeTab": { - "link": "链接", - "user": "用户" - }, - "linkModeDescription": "任何人只要拥有此链接即可加入,无需注册账号。", - "userModeDescription": "将主机共享给已拥有此主机访问权限的特定用户。如果他们还没有访问权限,请先将主机共享给他们,或者使用链接代替。共享后,会话将显示在他们的“连接”标签页中。", "permissionLevel": { - "label": "权限级别", "readOnly": "只读", - "readOnlyDescription": "可以观看直播,但无法打字或互动。", - "readWrite": "读写", - "readWriteDescription": "可以像会话所有者一样进行输入和互动。" - }, - "expiryLabel": "链接过期", - "createLinkButton": "创建链接", - "createShareButton": "与用户共享", - "searchUsersPlaceholder": "搜索用户...", - "noUsersFound": "未找到用户", - "linkCreated": "已创建共享链接", - "linkCopied": "链接已复制到剪贴板", - "copyLink": "复制链接", - "shareCreated": "会话已共享。它将出现在对方的“连接”标签页中。", - "shareFailed": "创建共享失败", - "userLacksHostAccess": "该用户目前没有访问此主机的权限。请先与其共享主机,或使用链接进行共享。", - "activeShares": "有效共享", - "noActiveShares": "此会话暂无活跃共享", - "revoke": "撤销", - "revokeConfirmTitle": "撤销此共享?", - "revokeConfirmDescription": "任何使用此共享的用户都将立即失去访问权限。", - "revoked": "共享已撤销", - "revokeFailed": "撤销共享失败", - "joinCount": "{{count}} 人加入", - "joinCount_other": "{{count}} 人加入", - "expiresAt": "过期时间 {{date}}", - "linkShareBadge": "链接", - "userShareBadge": "用户:{{username}}", - "loadSharesFailed": "无法加载共享" + "readWrite": "读写" + } }, "terminal": { "connect": "连接到主机", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "每日匿名发送用户、主机和功能使用情况统计数据,以帮助改进 Termix。绝不包含任何个人数据或连接详情。", "analyticsEnabledLockedDesc": "此设置受 ENABLE_TELEMETRY 环境变量锁定,无法在此处更改。", "updateAnalyticsFailed": "分析设置更新失败", - "sessionSharingGloballyEnabled": "允许会话共享", "auditForwardingTitle": "转发至 SIEM", "auditForwardingDesc": "将每条审计记录的副本以 NDJSON 格式发送给外部收集器。本地日志仍然是最终的权威来源。", "auditForwardingEnvNotice": "环境变量还可以配置转发;此处保存的 URL 优先。", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "持有者代币(已存储)", "auditForwardingSaved": "已保存的审核转发设置", "auditForwardingSaveError": "保存审计转发设置失败", - "sessionSharingGloballyEnabledDesc": "允许在整个实例范围内共享实时终端、RDP、VNC 和 Telnet 会话。禁用此功能将覆盖所有主机级别的共享设置。", - "updateSessionSharingFailed": "会话共享设置更新失败", "sessionTimeout": "会话超时", "hours": "小时", "sessionTimeoutRange": "最短 1 小时 · 最长 720 小时", diff --git a/src/ui/locales/translated/zh_TW.json b/src/ui/locales/translated/zh_TW.json index 3a5170eb1..b139c8d7e 100644 --- a/src/ui/locales/translated/zh_TW.json +++ b/src/ui/locales/translated/zh_TW.json @@ -667,7 +667,6 @@ "done_cli_link": "開啟 CLI 文檔" }, "nav": { - "collab": "會議", "home": "首頁", "terminal": "終端機", "localTerminal": "本地端", @@ -1036,8 +1035,6 @@ "enableAutoTmuxDesc": "透過在 tmux 中執行(tmux 必須安裝在主機上),可以在斷開連線和瀏覽器重新啟動後保持 shell 及其中執行的所有內容處於活動狀態。", "enableSessionLogging": "工作階段記錄", "enableSessionLoggingDesc": "記錄終端機工作階段輸出以供日後檢視", - "allowSessionSharing": "允許會話共享", - "allowSessionSharingDesc": "允許透過連結或其他方式分享此主機上的即時會話。", "localEchoLabel": "本地迴聲", "localEchoDesc": "在連線速度較慢的情況下,本地預測可安全列印的按鍵操作。密碼提示和控制輸入永遠不會被回顯。", "localEchoDefault": "使用全域設定", @@ -1571,137 +1568,11 @@ "join": "加入", "sharedSessionLabel": "{{hostName}} (共享)" }, - "collab": { - "createRoom": "新房間", - "roomName": "房間名稱", - "persistentRoom": "持久房間", - "persistentRoomHint": "會議結束後,持久會議室仍會保留在清單中,可重複使用。", - "noRooms": "暫無會議室。創建會議室即可開始會議。", - "openRoom": "打開", - "endRoom": "會議結束", - "leaveRoom": "離開", - "deleteMember": "消除", - "removeMember": "移除 {{name}}", - "removeMemberTitle": "移除此成員?", - "removeMemberDescription": "{{name}} 將立即失去該房間的存取權限。所有正在使用的 SSH 檢視器都將被斷開。", - "invite": "邀請", - "inviteTitle": "邀請成員", - "members": "成員", - "membersPanel": "房間成員和控制請求", - "membersWithCount": "成員({{count}})", - "online": "在線的", - "offline": "離線", - "you": "(你)", - "hostBadge": "主持人", - "presenterBadge": "呈現", - "present": "展示", - "presentTitle": "登上舞台", - "presentHost": "主持人", - "searchHosts": "搜尋主機", - "noHostsFound": "未找到可用主機。", - "presentProtocol": "協定", - "stopPresenting": "停止演示", - "takeOver": "接管", - "takeOverConfirmTitle": "替換當前簡報?", - "takeOverDescription": "{{name}} 正在直播中。接管直播將會中斷所有人與該直播間的連結。", - "emptyStage": "目前無人發言。請上台分享您的演講內容。", - "stageLoading": "連接到舞台…", - "stageEnded": "演講結束了", - "roomEnded": "會議結束", - "presenterLabel": "{{name}} 正在呈現", - "guestLink": "訪客連結", - "guestLinkOn": "訪客連結已開啟。任何擁有該連結的人都可以觀看舞台表演。", - "guestLinkOff": "訪客連結已關閉。", - "copyLink": "複製連結", - "rotateLink": "旋轉連結", - "disableLink": "停用連結", - "rotateLinkConfirmTitle": "旋轉訪客連結?", - "rotateLinkDescription": "當前連線將立即停止工作。已連線的SSH訪客將會被斷開連線。", - "disableLinkConfirmTitle": "禁用訪客連結?", - "disableLinkDescription": "匿名 SSH 檢視者將立即被斷開連接,目前連結將停止運作。", - "linkCopied": "連結已複製", - "roles": "角色", - "users": "使用者", - "searchUsers": "搜尋用戶", - "endConfirmTitle": "結束會議嗎?", - "endDescription": "{{name}} 將對所有人關閉,且無法重新開啟。", - "endPersistentDescription": "{{name}} 中的目前示範將停止。該房間仍可供再次使用。", - "deleteRoom": "刪除房間", - "deleteConfirmTitle": "刪除這個房間?", - "deleteDescription": "{{name}} 將會永久刪除,且無法撤銷。", - "invitedTo": "您受邀參加“{{name}}”", - "guest": { - "title": "會議", - "waiting": "等待主持人…", - "linkInvalid": "此訪客連結無效或會議已結束" - }, - "reopenFromPanel": "此會議標籤頁已過期。請從「會議」面板重新開啟會議室。", - "requestControl": "請求控制", - "cancelControlRequest": "取消請求", - "controlQueue": "控制請求", - "noControlRequests": "沒有待處理的請求。", - "grantControlTo": "將控制權交給 {{name}}", - "dismissControlRequest": "駁回 {{name}}的控制請求", - "releaseControl": "發布控制", - "grantControl": "賦予控制權", - "revokeControl": "奪回控制權", - "controlRequestedBy": "{{name}} 請求控制權", - "grant": "授予", - "controlBadge": "掌控之中", - "youArePresenting": "您正在另一個標籤頁中進行示範。請停止演示,將演示權交給對方。", - "created": "創建房間", - "invited": "受邀成員", - "roomTab": "會議" - }, "sessionSharing": { - "guestView": { - "loading": "正在連接到共享會話...", - "linkInvalid": "此分享連結無效、已過期或已撤銷。", - "rateLimited": "嘗試次數過多,請稍後再試。", - "sessionEnded": "本次會議已結束", - "readOnlyBadge": "僅查看", - "ownerLabel": "房東" - }, - "modalTitle": "分享會話", - "shareButton": "分享", - "notReadyToShare": "會話尚未準備好分享", - "modeTab": { - "link": "關聯", - "user": "使用者" - }, - "linkModeDescription": "任何人只要擁有此連結即可加入,無需註冊帳號。", - "userModeDescription": "將主機共用給已擁有此主機存取權限的特定使用者。如果他們還沒有存取權限,請先將主機分享給他們,或使用連結代替。共享後,會話將顯示在他們的“連接”選項卡中。", "permissionLevel": { - "label": "權限等級", "readOnly": "只讀", - "readOnlyDescription": "可以觀看直播,但無法打字或互動。", - "readWrite": "讀寫", - "readWriteDescription": "可以像會話擁有者一樣進行輸入和互動。" - }, - "expiryLabel": "連結過期", - "createLinkButton": "建立連結", - "createShareButton": "與用戶分享", - "searchUsersPlaceholder": "搜尋用戶...", - "noUsersFound": "未找到用戶", - "linkCreated": "已建立分享連結", - "linkCopied": "連結已複製到剪貼簿", - "copyLink": "複製連結", - "shareCreated": "會話已共用。它將出現在對方的「連線」標籤中。", - "shareFailed": "建立共享失敗", - "userLacksHostAccess": "該使用者目前沒有存取此主機的權限。請先與其共享主機,或使用連結進行共享。", - "activeShares": "活躍股", - "noActiveShares": "本會話無活躍份額", - "revoke": "撤銷", - "revokeConfirmTitle": "撤銷此份額?", - "revokeConfirmDescription": "任何使用此共用資料夾的使用者都將立即失去存取權限。", - "revoked": "股份撤銷", - "revokeFailed": "撤銷股份失敗", - "joinCount": "{{count}} 加入", - "joinCount_other": "{{count}} 加入", - "expiresAt": "過期時間 {{date}}", - "linkShareBadge": "關聯", - "userShareBadge": "使用者: {{username}}", - "loadSharesFailed": "無法加載活動份額" + "readWrite": "讀寫" + } }, "terminal": { "connect": "連線至主機", @@ -2239,7 +2110,6 @@ "analyticsEnabledDesc": "每日匿名傳送使用者、主機和功能使用統計數據,以協助改善 Termix。絕不包含任何個人資料或連結詳情。", "analyticsEnabledLockedDesc": "此設定受 ENABLE_TELEMETRY 環境變數鎖定,無法在此變更。", "updateAnalyticsFailed": "分析設定更新失敗", - "sessionSharingGloballyEnabled": "允許會話共享", "auditForwardingTitle": "轉寄至 SIEM", "auditForwardingDesc": "將每個審計記錄的副本以 NDJSON 格式傳送給外部收集器。本地日誌仍然是最終的權威來源。", "auditForwardingEnvNotice": "環境變數也可以配置轉送;此處儲存的 URL 優先。", @@ -2247,8 +2117,6 @@ "auditForwardingTokenStored": "持有者代幣(已儲存)", "auditForwardingSaved": "已儲存的審核轉送設定", "auditForwardingSaveError": "審核轉送設定保存失敗", - "sessionSharingGloballyEnabledDesc": "允許在整個執行個體範圍內共用即時終端、RDP、VNC 和 Telnet 會話。停用此功能將覆蓋所有主機層級的共用設定。", - "updateSessionSharingFailed": "會話共享設定更新失敗", "sessionTimeout": "工作階段逾時", "hours": "小時", "sessionTimeoutRange": "最短 1 小時,最長 720 小時", diff --git a/src/ui/plugin-host/loader.ts b/src/ui/plugin-host/loader.ts index 550b1458b..b1873e533 100644 --- a/src/ui/plugin-host/loader.ts +++ b/src/ui/plugin-host/loader.ts @@ -350,6 +350,26 @@ async function fetchGuestPlugins(): Promise { return Array.isArray(body) ? (body as PluginSummary[]) : []; } +let guestViewsRequest: Promise | null = null; + +/** + * The `?view=` names enabled guest plugins serve to anonymous pages, so the + * entry point can tell a guest link from a signed-in full-screen view + * without knowing any plugin. + */ +export function fetchGuestViews(): Promise { + guestViewsRequest ??= fetchGuestPlugins() + .then((plugins) => + plugins.flatMap( + (plugin) => + (plugin.contributes as { guestViews?: string[] } | undefined) + ?.guestViews ?? [], + ), + ) + .catch(() => []); + return guestViewsRequest; +} + async function fetchPreLoginPlugins(): Promise { const response = await fetch(getBackendUrl("/plugins/public-manifest")); if (!response.ok) return []; diff --git a/src/ui/plugin-host/shell-bridge.ts b/src/ui/plugin-host/shell-bridge.ts index 365e1ce47..0934f78cd 100644 --- a/src/ui/plugin-host/shell-bridge.ts +++ b/src/ui/plugin-host/shell-bridge.ts @@ -99,6 +99,7 @@ export const tabsApi = { openTab: shell.openTab, openSingletonTab: shell.openSingletonTab, closeTab: shell.closeTab, + openRailView: shell.openRailView, getLayout: () => layoutProvider?.getLayout() ?? null, applyLayout: (layout: Layout, options?: { name?: string }) => layoutProvider diff --git a/src/ui/shell/TabBar.tsx b/src/ui/shell/TabBar.tsx index 26d2bbeee..10429df77 100644 --- a/src/ui/shell/TabBar.tsx +++ b/src/ui/shell/TabBar.tsx @@ -27,10 +27,9 @@ import { Minimize2, FolderOpen, PanelRight, - Share2, } from "lucide-react"; import { tabIcon } from "@/shell/tabUtils"; -import { isSessionTabType, isShareableTabType } from "@/shell/tab-registry"; +import { isSessionTabType } from "@/shell/tab-registry"; import { isElectron } from "@/lib/electron"; import type { Tab, TabType, SplitMode } from "@/types/ui-types"; import { SPLIT_MODES, PANE_COUNTS } from "@/lib/theme"; @@ -43,11 +42,6 @@ function isConnectionTab(type: TabType): boolean { return isSessionTabType(type); } -/** Connection tabs that also offer "Share session". */ -function isShareableConnectionTab(type: TabType): boolean { - return isConnectionTab(type) && isShareableTabType(type); -} - export function TabBar({ tabs, activeTabId, @@ -63,7 +57,6 @@ export function TabBar({ onRemoveFromSplit, onRenameTab, onOpenFileManager, - onOpenShare, isAppFullscreen, onToggleAppFullscreen, rightDockOpen, @@ -84,7 +77,6 @@ export function TabBar({ onRemoveFromSplit: (tabId: string) => void; onRenameTab?: (tabId: string, newLabel: string) => void; onOpenFileManager?: (tabId: string) => void; - onOpenShare?: (tabId: string) => void; isAppFullscreen: boolean; onToggleAppFullscreen: () => void; rightDockOpen?: boolean; @@ -472,19 +464,6 @@ export function TabBar({ )} - {isShareableConnectionTab(tab.type) && onOpenShare && ( - - )}